Skip to content
Merged
7 changes: 7 additions & 0 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import Main from './pages/external/Main.jsx';
import Intro from './pages/external/Intro.jsx';
import Leaders from './pages/external/Leaders.jsx';
import Portfolio from './pages/external/Portfolio.jsx';
import MonthlyReport from './pages/external/MonthlyReport.jsx';
import MonthlyReportDetail from './pages/external/MonthlyReportDetail.jsx';

import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
Expand All @@ -31,6 +33,11 @@ function App() {
<Route path="/main/intro" element={<Intro />} />
<Route path="/main/leaders" element={<Leaders />} />
<Route path="/main/portfolio" element={<Portfolio />} />
<Route path="/main/monthly-report" element={<MonthlyReport />} />
<Route
path="/main/monthly-report-detail"
element={<MonthlyReportDetail />}
/>
<Route path="/login" element={<Login />} />
<Route path="/signup" element={<SignUp />} />
<Route path="/oauth/success" element={<OAuthSuccess />} />
Expand Down
Binary file added frontend/src/assets/external/monthly-report.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
54 changes: 54 additions & 0 deletions frontend/src/components/external/Content.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import styles from './Content.module.css';
import { pages } from '../../utils/reportContent';
import { useState } from 'react';

const Content = () => {
const [current, setCurrent] = useState(0);
const goPrev = () => {
if (current > 1) setCurrent(current - 2);
};

const goNext = () => {
if (current < pages.length - 2) setCurrent(current + 2);
};
Comment on lines +6 to +13
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

페이지 이동 로직과 우측 화살표 비활성 조건 불일치

현재 구현에서는:

  • goNextcurrent < pages.length - 2일 때만 current를 +2 하여 마지막 spread(예: 길이 22일 때 current = 20)까지만 이동하고,
  • 우측 화살표 disabled 조건은 current === pages.length - 1로 되어 있어, 실제로는 마지막 spread에서도 버튼이 비활성화되지 않습니다(클릭해도 state는 안 바뀌지만, UI 상으로는 계속 활성처럼 보임).

또한 setCurrent(current ± 2)는 콜백이 아니라 클로저 값을 쓰고 있어, 향후 로직 변경 시 예기치 못한 동작 여지가 있습니다.

아래처럼 boundary를 일관되게 맞추고, 콜백 형태로 상태를 갱신하는 편이 더 안전하고 직관적입니다.

-  const [current, setCurrent] = useState(0);
-  const goPrev = () => {
-    if (current > 1) setCurrent(current - 2);
-  };
-
-  const goNext = () => {
-    if (current < pages.length - 2) setCurrent(current + 2);
-  };
+  const [current, setCurrent] = useState(0);
+
+  const goPrev = () => {
+    setCurrent((prev) => Math.max(prev - 2, 0));
+  };
+
+  const goNext = () => {
+    setCurrent((prev) =>
+      Math.min(prev + 2, pages.length - 2),
+    );
+  };
@@
-        <button
-          className={styles.arrowBtn}
-          onClick={goNext}
-          disabled={current === pages.length - 1}
-        >
+        <button
+          className={styles.arrowBtn}
+          onClick={goNext}
+          disabled={current >= pages.length - 2}
+        >

추가로:

  • 현재 구현은 pages.length가 짝수(항상 2장 단위로 끝나는 경우)라는 전제를 깔고 있으므로, 추후 API에서 페이지 수가 홀수일 가능성이 있다면 마지막 한 장에 대한 처리(예: 오른쪽 이미지는 조건부 렌더링)를 고려해 두면 좋겠습니다.
  • 접근성을 위해 좌우 화살표 버튼에 aria-label="이전 페이지", aria-label="다음 페이지"를 부여하는 것도 추천드립니다.

Also applies to: 20-21, 25-48

🤖 Prompt for AI Agents
In frontend/src/components/external/Content.jsx around lines 6–13 (and also
apply same fix to 20–21, 25–48), the page navigation boundary logic and
disabled-state checks are inconsistent and setState uses closure values; compute
a single maxIndex = pages.length - (pages.length % 2 === 0 ? 2 : 1) and use that
for both navigation guards and button disabled checks, update goPrev/goNext to
use functional state updates (setCurrent(prev => Math.max(0, Math.min(maxIndex,
prev ± 2)))), ensure the right-hand page/image is conditionally rendered when
pages.length is odd, and add aria-label="이전 페이지" / aria-label="다음 페이지" to the
arrow buttons for accessibility.

return (
<div className={styles.content}>
<div className={styles.progressWrapper}>
<div
className={styles.progressBar}
style={{
width: `${((current + 2) / pages.length) * 100}%`,
}}
/>
</div>

<div className={styles.container}>
{/* 좌측 화살표 */}
<button
className={styles.arrowBtn}
onClick={goPrev}
disabled={current === 0}
>
<span className={styles.leftArrowIcon}></span>
</button>

{/* 이미지 페이지 */}
<div className={styles.pageSection}>
<img src={pages[current]} className={styles.page} alt="report" />
<img src={pages[current + 1]} className={styles.page} alt="report" />
</div>

{/* 우측 화살표 */}
<button
className={styles.arrowBtn}
onClick={goNext}
disabled={current >= pages.length - 2}
>
<span className={styles.rightArrowIcon}></span>
</button>
</div>
</div>
);
};

export default Content;
84 changes: 84 additions & 0 deletions frontend/src/components/external/Content.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
.content {
display: flex;
flex-direction: column;
gap: 40px;
}

/* 프로그레스바 전체 영역 */
.progressWrapper {
width: 100%;
height: 6px;
position: relative;
overflow: hidden;
}

/* 실제 진행 바 */
.progressBar {
height: 100%;
background: linear-gradient(90deg, #2563eb, #3b82f6);
transition: width 0.35s ease;
}

.container {
display: flex;
align-items: center;
gap: 8px;
justify-content: space-around;
}

.arrowBtn {
width: 41px;
height: 41px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.9);
border: none;
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.15);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
padding: 0;
}

/* 화살표 */
.leftArrowIcon {
width: 10px;
height: 10px;
border-left: 3px solid #3b82f6;
border-bottom: 3px solid #3b82f6;
transform: rotate(45deg);
margin-left: 4px; /* 가운데 보정 */
}

.rightArrowIcon {
width: 10px;
height: 10px;
border-left: 3px solid #3b82f6;
border-bottom: 3px solid #3b82f6;
transform: rotate(-135deg);
margin-right: 4px; /* 가운데 보정 */
}

.arrowBtn:disabled {
cursor: default;
opacity: 0.4;
}

.page {
width: 100%;
max-width: 520px;
aspect-ratio: 3 / 4; /* 이미지 비율 */
height: auto;
}

@media (max-width: 1200px) {
.page {
max-width: 420px;
}
}

@media (max-width: 950px) {
.page {
max-width: 300px;
}
}
25 changes: 25 additions & 0 deletions frontend/src/components/external/Report.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import styles from './Report.module.css';
import { useNavigate } from 'react-router-dom';
import reportCover from '../../assets/external/report/report-cover.png';

const Report = () => {
const nav = useNavigate();
return (
<div className={styles.container}>
<div
className={styles.card}
onClick={() => nav('/main/monthly-report-detail')}
// API 연동 후 경로 변경 예정
style={{
background: `url(${reportCover}) center no-repeat`,
backgroundSize: 'contain',
}}
></div>
<span className={styles.title}>
세투연의 한 달 활동 기록과 주요 성과 요약
</span>
</div>
);
};

export default Report;
21 changes: 21 additions & 0 deletions frontend/src/components/external/Report.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.container {
display: flex;
flex-direction: column;
gap: 20px;
width: 270px;
}

.card {
width: 270px;
height: 345px;
border-radius: 8px;
}

.title {
font-weight: 800;
font-size: 18px;
line-height: 100%;
letter-spacing: 0%;
color: rgba(255, 255, 255, 1);
word-break: keep-all;
}
3 changes: 2 additions & 1 deletion frontend/src/pages/external/External.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
}

.header {
background: url('../../assets/external-detail-image.png') center no-repeat;
background: url('../../assets/external/external-detail-image.png') center
no-repeat;
height: 25%;
display: flex;
flex-direction: column;
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/pages/external/Main.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import styles from './Main.module.css';
import image from '../../assets/external-image.png';
import image from '../../assets/external/external-image.png';
import Logo from '../../assets/logo.png';
import { Link } from 'react-router-dom';

Expand All @@ -18,6 +18,9 @@ const Main = () => {
<li>
<Link to="/main/portfolio">운용 포트폴리오</Link>
</li>
<li>
<Link to="/main/monthly-report">월간 세투연</Link>
</li>
<li>
<Link to="/">웹사이트</Link>
</li>
Expand Down
37 changes: 37 additions & 0 deletions frontend/src/pages/external/MonthlyReport.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import styles from './MonthlyReport.module.css';
import logo from '../../assets/logo.png';
import Report from '../../components/external/Report';

const MonthlyReport = () => {
return (
<div className={styles.page}>
<div className={styles.header}>
<div className={styles.logoSection}>
<img src={logo} alt="세종투자연구회" className={styles.logo} />
<span className={styles.logoName}>월간 세투연</span>
</div>
<h1 className={styles.title}>
매월 업데이트되는{' '}
<strong className={styles.strong}>세투연 콘텐츠</strong>를 <br /> 한
곳에 모았습니다.
</h1>
<h2 className={styles.subTitle}>
지난 한 달의 활동과 자료들을 아카이브 형식으로 정리했어요.
</h2>
</div>
<div className={styles.countSection}>
<span className={styles.count}>
<strong style={{ color: '#339FFF' }}>4개</strong>의 게시물
</span>
</div>
<div className={styles.reportSection}>
<Report />
<Report />
<Report />
<Report />
</div>
</div>
);
};

export default MonthlyReport;
90 changes: 90 additions & 0 deletions frontend/src/pages/external/MonthlyReport.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
.page {
display: flex;
flex-direction: column;
width: 100vw;
height: 100vh;
}

.header {
background:
linear-gradient(rgba(0, 0, 0, 0.82), rgba(0, 0, 0, 0.82)),
url('../../assets/external/monthly-report.png') center no-repeat;
min-height: 300px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
}

.logoSection {
display: inline-flex;
height: 56px;
padding: 16px;
justify-content: center;
align-items: center;
gap: 10px;
border-radius: 37px;
background: rgba(51, 159, 255, 0.12);
}

.logo {
width: 26px;
height: 26px;
filter: brightness(0) saturate(1) invert(82%) sepia(12%) saturate(400%)
hue-rotate(175deg) brightness(110%) contrast(90%);
}

.logoName {
color: rgba(192, 225, 255, 1);
font-size: 20px;
font-style: normal;
line-height: 100%;
letter-spacing: -0.8px;
}

.title {
color: #fff;
text-align: center;
font-size: 40px;
font-weight: normal;
line-height: 150%;
margin: 0;
word-break: keep-all;
}

.strong {
color: #339fff;
font-weight: normal;
}

.subTitle {
color: #d9d9d9;
font-size: 20px;
font-weight: 400;
line-height: 150%;
margin: 0;
}

.countSection {
background-color: #000000;
padding-left: 50px;
padding-top: 30px;
}

.count {
font-weight: 600;
font-size: 20px;
line-height: 150%;
letter-spacing: 0%;
color: #7e7e7e;
}

.reportSection {
padding: 30px 50px;
background-color: #000000;
flex: 1;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(270px, 1fr));
gap: 10px;
}
24 changes: 24 additions & 0 deletions frontend/src/pages/external/MonthlyReportDetail.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import styles from './MonthlyReportDetail.module.css';
import logo from '../../assets/logo.png';
import { useNavigate } from 'react-router-dom';
import Content from '../../components/external/Content';

const MonthlyReportDetail = () => {
const nav = useNavigate();
return (
<div className={styles.container}>
<header
className={styles.header}
onClick={() => nav('/main/monthly-report')}
>
<img src={logo} alt="로고" className={styles.logo} />
<span className={styles.title}>월간 세투연</span>
</header>
<div className={styles.content}>
<Content />
</div>
</div>
);
};

export default MonthlyReportDetail;
Loading