Skip to content

Commit 0f98433

Browse files
authored
[FRONTEND] 컨벤션 fix (#96)
네, 방금 적용하신 React 프로젝트 파일 네이밍 컨벤션을 정리해 드립니다. 앞으로 팀원들과 협업할 때 이 기준을 따르면 프로젝트 구조가 훨씬 일관성 있게 유지될 것입니다. 🎨 변경된 프론트엔드 코딩 컨벤션 (File Naming) 목적: ESLint를 통해 파일 이름 규칙을 강제하여, 일관성 있는 프로젝트 구조를 유지함. 1. React 컴포넌트 파일 (.tsx) 규칙: PascalCase (대문자로 시작) 적용 대상: *.tsx 확장자를 가진 모든 파일 (단, *.d.tsx 제외) 예시: ✅ GlossaryModal.tsx ✅ UserProfile.tsx ❌ glossaryModal.tsx (소문자 시작 불가) ❌ user-profile.tsx (케밥 케이스 불가) 이유: React 컴포넌트는 코드 내에서 <GlossaryModal />처럼 대문자로 사용되므로, 파일명과 컴포넌트명을 일치시키는 것이 관례입니다. 2. 일반 함수 / Hooks / 유틸 파일 (.ts) 규칙: camelCase (소문자로 시작, 중간 단어 대문자) 적용 대상: *.ts 확장자를 가진 모든 파일 (단, *.d.ts, vite-env.ts 등 설정 파일 제외) 예시: ✅ useModal.ts (Hooks) ✅ apiClient.ts (Utils) ❌ UseModal.ts (Hooks는 보통 소문자 use로 시작) ❌ APIClient.ts ⚡️ 적용 방법 및 확인 검사 실행: 터미널에서 npm run lint를 입력하면 컨벤션에 맞지 않는 파일들이 에러로 표시됩니다. 자동 감지: VS Code 등 에디터에서 ESLint 플러그인을 사용 중이라면, 파일 생성 시 규칙에 어긋날 경우 즉시 빨간 줄이 그어집니다. 지금 바로 수정해야 할 파일들 (예시): src/components/.../glossaryModal.tsx ➡️ GlossaryModal.tsx src/pages/community.tsx ➡️ Community.tsx src/pages/guide.tsx ➡️ Guide.tsx
1 parent 7eb27ca commit 0f98433

File tree

3 files changed

+296
-1
lines changed

3 files changed

+296
-1
lines changed
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import { useState, useEffect } from "react";
2+
import Modal from "../../ui/Modal/Modal";
3+
import LoadingSpinner from "../LoadingSpinner/LoadingSpinner";
4+
import styles from "./CompatibiliityCheckModal.module.css";
5+
6+
interface CompatibilityCheckModalProps {
7+
isOpen: boolean;
8+
onClose: () => void;
9+
}
10+
11+
interface CheckItem {
12+
id: string;
13+
label: string;
14+
status: "pending" | "loading" | "complete";
15+
}
16+
17+
const checkItems: CheckItem[] = [
18+
{ id: "monitor", label: "물리 규격 호환성", status: "pending" },
19+
{ id: "resolution", label: "전원 호환", status: "pending" },
20+
{ id: "memory-type", label: "메모리 타입", status: "pending" },
21+
{ id: "memory-speed", label: "메모리 속도", status: "pending" },
22+
{ id: "graphics", label: "파워 용량", status: "pending" },
23+
{ id: "graphics-power", label: "그래픽 카드 /파워 호환", status: "pending" },
24+
{ id: "panel-type", label: "파워 포트 인증", status: "pending" },
25+
{
26+
id: "update-monitor",
27+
label: "수평 리프레쉬레이트 규격 지원",
28+
status: "pending",
29+
},
30+
{ id: "vertical-refresh", label: "스토리지 형식 호환", status: "pending" },
31+
{ id: "case-form", label: "케이스 폼팩터 포함성", status: "pending" },
32+
{ id: "rgb", label: "RGB/ARGB 커넥터 호환성", status: "pending" },
33+
{ id: "multimedia", label: "운영체제 및 드라이버 지원", status: "pending" },
34+
];
35+
36+
export default function CompatibilityCheckModal({
37+
isOpen,
38+
onClose,
39+
}: CompatibilityCheckModalProps) {
40+
const [items, setItems] = useState<CheckItem[]>(checkItems);
41+
const [currentCheckIndex, setCurrentCheckIndex] = useState(-1);
42+
const [isChecking, setIsChecking] = useState(false);
43+
44+
useEffect(() => {
45+
if (isOpen && !isChecking) {
46+
// 모달이 열리면 자동으로 체크 시작
47+
startChecking();
48+
}
49+
// eslint-disable-next-line react-hooks/exhaustive-deps
50+
}, [isOpen]);
51+
52+
const startChecking = () => {
53+
setIsChecking(true);
54+
setCurrentCheckIndex(0);
55+
setItems(checkItems.map((item) => ({ ...item, status: "pending" })));
56+
};
57+
58+
useEffect(() => {
59+
if (!isChecking || currentCheckIndex === -1) return;
60+
61+
if (currentCheckIndex >= items.length) {
62+
setIsChecking(false);
63+
return;
64+
}
65+
66+
// 현재 항목을 loading으로 변경
67+
setItems((prev) =>
68+
prev.map((item, idx) =>
69+
idx === currentCheckIndex ? { ...item, status: "loading" } : item
70+
)
71+
);
72+
73+
// 0.5초 후 complete로 변경하고 다음 항목으로
74+
const timer = setTimeout(() => {
75+
setItems((prev) =>
76+
prev.map((item, idx) =>
77+
idx === currentCheckIndex ? { ...item, status: "complete" } : item
78+
)
79+
);
80+
setCurrentCheckIndex((prev) => prev + 1);
81+
}, 500);
82+
83+
return () => clearTimeout(timer);
84+
}, [currentCheckIndex, isChecking, items.length]);
85+
86+
const renderCheckStatus = (item: CheckItem) => {
87+
if (item.status === "loading") {
88+
return (
89+
<div className={styles.checkItem}>
90+
<LoadingSpinner size="small" color="#FF5525" variant="spinner" />
91+
</div>
92+
);
93+
}
94+
if (item.status === "complete") {
95+
return (
96+
<div className={`${styles.checkItem} ${styles.complete}`}>
97+
<div className={styles.completeCircle}>
98+
<svg
99+
width="20"
100+
height="20"
101+
viewBox="0 0 20 20"
102+
fill="none"
103+
xmlns="http://www.w3.org/2000/svg"
104+
>
105+
<circle cx="10" cy="10" r="9" stroke="#4CAF50" strokeWidth="2" />
106+
<path
107+
d="M6 10L9 13L14 7"
108+
stroke="#4CAF50"
109+
strokeWidth="2"
110+
strokeLinecap="round"
111+
strokeLinejoin="round"
112+
/>
113+
</svg>
114+
</div>
115+
</div>
116+
);
117+
}
118+
return (
119+
<div className={styles.checkItem}>
120+
<div className={styles.pendingCircle}></div>
121+
</div>
122+
);
123+
};
124+
125+
const allComplete = items.every((item) => item.status === "complete");
126+
127+
return (
128+
<Modal isOpen={isOpen} onClose={onClose} title="호환성 체크" size="lg">
129+
<div className={styles.container}>
130+
<div className={styles.grid}>
131+
{items.map((item) => (
132+
<div key={item.id} className={styles.gridItem}>
133+
{renderCheckStatus(item)}
134+
<div className={styles.label}>{item.label}</div>
135+
</div>
136+
))}
137+
</div>
138+
139+
<div className={styles.statusMessage}>
140+
{isChecking && !allComplete && <p>호환성 체크 중입니다...</p>}
141+
{allComplete && (
142+
<p className={styles.completeMessage}>
143+
모든 호환성 체크가 완료되었습니다!
144+
</p>
145+
)}
146+
</div>
147+
148+
<button
149+
className={styles.pdfButton}
150+
disabled={!allComplete}
151+
onClick={() => {
152+
// PDF 다운로드 로직
153+
console.log("PDF 다운로드");
154+
}}
155+
>
156+
PDF 내보내기
157+
</button>
158+
</div>
159+
</Modal>
160+
);
161+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
.container {
2+
display: flex;
3+
flex-direction: column;
4+
gap: var(--spacing-8);
5+
padding: var(--spacing-4);
6+
}
7+
8+
.grid {
9+
display: grid;
10+
grid-template-columns: repeat(6, 1fr);
11+
gap: var(--spacing-8);
12+
margin-bottom: var(--spacing-6);
13+
}
14+
15+
.gridItem {
16+
display: flex;
17+
flex-direction: column;
18+
align-items: center;
19+
gap: var(--spacing-3);
20+
}
21+
22+
.checkItem {
23+
width: 60px;
24+
height: 60px;
25+
display: flex;
26+
align-items: center;
27+
justify-content: center;
28+
position: relative;
29+
}
30+
31+
.pendingCircle {
32+
width: 40px;
33+
height: 40px;
34+
border-radius: 50%;
35+
border: 2px solid #e0e0e0;
36+
background-color: transparent;
37+
}
38+
39+
.complete .completeCircle {
40+
animation: scaleIn 0.3s ease-out;
41+
}
42+
43+
.label {
44+
font-size: var(--font-size-sm);
45+
color: var(--text-secondary);
46+
text-align: center;
47+
line-height: 1.4;
48+
min-height: 2.8em;
49+
display: flex;
50+
align-items: center;
51+
justify-content: center;
52+
}
53+
54+
.statusMessage {
55+
min-height: 60px;
56+
padding: var(--spacing-6);
57+
background-color: var(--bg-secondary);
58+
border-radius: var(--border-radius-lg);
59+
display: flex;
60+
align-items: center;
61+
justify-content: center;
62+
text-align: center;
63+
}
64+
65+
.statusMessage p {
66+
margin: 0;
67+
font-size: var(--font-size-base);
68+
color: var(--text-secondary);
69+
}
70+
71+
.completeMessage {
72+
color: #4caf50 !important;
73+
font-weight: var(--font-weight-semibold);
74+
}
75+
76+
.pdfButton {
77+
width: 200px;
78+
margin: 0 auto;
79+
padding: var(--spacing-3) var(--spacing-6);
80+
background-color: #ff5525;
81+
color: white;
82+
border: none;
83+
border-radius: var(--border-radius-lg);
84+
font-size: var(--font-size-base);
85+
font-weight: var(--font-weight-semibold);
86+
cursor: pointer;
87+
transition: all var(--transition-fast);
88+
}
89+
90+
.pdfButton:hover:not(:disabled) {
91+
background-color: #e64a1f;
92+
transform: translateY(-2px);
93+
box-shadow: 0 4px 12px rgba(255, 85, 37, 0.3);
94+
}
95+
96+
.pdfButton:disabled {
97+
background-color: #ccc;
98+
cursor: not-allowed;
99+
opacity: 0.6;
100+
}
101+
102+
@keyframes scaleIn {
103+
from {
104+
transform: scale(0);
105+
opacity: 0;
106+
}
107+
108+
to {
109+
transform: scale(1);
110+
opacity: 1;
111+
}
112+
}
113+
114+
/* 반응형 디자인 */
115+
@media (max-width: 768px) {
116+
.grid {
117+
grid-template-columns: repeat(3, 1fr);
118+
gap: var(--spacing-4);
119+
}
120+
121+
.checkItem {
122+
width: 50px;
123+
height: 50px;
124+
}
125+
126+
.pendingCircle {
127+
width: 35px;
128+
height: 35px;
129+
}
130+
131+
.label {
132+
font-size: var(--font-size-xs);
133+
}
134+
}

frontend/src/components/ui/Modal/Modal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useEffect } from "react";
22
import { createPortal } from "react-dom";
3-
import styles from "./Modal.module.css";
3+
import styles from "./modal.module.css";
44

55
interface ModalProps {
66
isOpen: boolean;

0 commit comments

Comments
 (0)