diff --git a/frontend/src/pages/qna/QnADetailPage.js b/frontend/src/pages/qna/QnADetailPage.js index bb93229..8dbf4af 100644 --- a/frontend/src/pages/qna/QnADetailPage.js +++ b/frontend/src/pages/qna/QnADetailPage.js @@ -674,7 +674,7 @@ function QnADetailPage() { placeholder="댓글을 입력해주세요..." value={commentText} onChange={e => setCommentText(e.target.value)} - onKeyDown={e => { if (e.key === 'Enter') handleCommentSubmit(); }} + onKeyDown={e => { if (e.key === 'Enter' && !e.nativeEvent.isComposing) handleCommentSubmit(); }} onPaste={handlePaste} disabled={isSubmitting} /> diff --git a/frontend/src/pages/qna/QnAListPage.js b/frontend/src/pages/qna/QnAListPage.js index 7f0e696..c0f0bf8 100644 --- a/frontend/src/pages/qna/QnAListPage.js +++ b/frontend/src/pages/qna/QnAListPage.js @@ -528,27 +528,9 @@ function QnAListPage() { if (!res.ok) throw new Error(); const json = await res.json(); if (json.isSuccess) { - if (isStaff) { - await authFetch(`/api/questions/${questionId}/status`, { method: 'PATCH' }); - } - const newComment = { - commentId: json.result.commentId, - displayName: json.result.displayName, - content: json.result.content, - }; - const update = (list) => list.map(q => - q.questionId === questionId - ? { - ...q, - isResolved: isStaff ? true : q.isResolved, - previewComments: [...(q.previewComments ?? []), newComment], - commentCount: (q.commentCount ?? 0) + 1 - } - : q - ); - setPopularQuestions(update); - setUnresolvedQuestions(update); - setResolvedQuestions(update); + // NOTE: 댓글 작성으로 인한 '해결됨' 자동 전환 없음 (해결 처리는 운영진 수동 조작만 허용). + // previewComments / commentCount UI 갱신도 SSE comment-created 이벤트에서 단일 처리. + // 여기서 직접 상태를 업데이트하면 SSE 이벤트와 중복되어 댓글이 2개 표시되므로 제거. setCommentInputs(prev => ({ ...prev, [questionId]: '' })); setCommentImages(prev => ({ ...prev, [questionId]: [] })); setCommentImagePreviews(prev => ({ ...prev, [questionId]: [] })); @@ -790,7 +772,8 @@ function QnAListPage() { {/* ── 질문 목록 ── */}
{displayedQuestions.map(question => ( -
navigate(`/sessions/${sessionId}/questions/${question.questionId}`)}> {/* 질문 헤더 */} @@ -902,7 +885,7 @@ function QnAListPage() { placeholder="댓글을 입력해주세요..." value={commentInputs[question.questionId] || ''} onChange={e => handleCommentChange(question.questionId, e.target.value)} - onKeyDown={e => { if (e.key === 'Enter') handleCommentSubmit(e, question.questionId); }} + onKeyDown={e => { if (e.key === 'Enter' && !e.nativeEvent.isComposing) handleCommentSubmit(e, question.questionId); }} onPaste={e => handleCommentPaste(e, question.questionId)} autoFocus /> @@ -960,7 +943,7 @@ function QnAListPage() { value={newQuestion} onChange={e => setNewQuestion(e.target.value)} onKeyDown={e => { - if (e.key === 'Enter') isStaff ? handleNewUnderstandCheck() : handleNewQuestion(); + if (e.key === 'Enter' && !e.nativeEvent.isComposing) isStaff ? handleNewUnderstandCheck() : handleNewQuestion(); }} onPaste={handleNewQuestionPaste} disabled={isSubmitting} diff --git a/frontend/src/pages/qna/QnAListPage.module.css b/frontend/src/pages/qna/QnAListPage.module.css index 95ffedf..10ea886 100644 --- a/frontend/src/pages/qna/QnAListPage.module.css +++ b/frontend/src/pages/qna/QnAListPage.module.css @@ -1,5 +1,11 @@ /* ── 페이지 레이아웃 ── */ +/* .pageWrapper { + background: var(--gray50); + min-height: 100vh; +} */ + .page { + /* background: var(--gray50); */ display: flex; flex-direction: column; min-height: 100vh; @@ -19,6 +25,7 @@ margin: 10px; color: var(--black); padding-top: 60px; + padding-bottom: 40px; font-weight: 700; line-height: normal; } @@ -108,17 +115,20 @@ /* ── 이해도 바 ── */ .understandBar { + padding: 8px 16px; margin-top: 20px; - margin-bottom: 20px; + margin-bottom: 30px; display: flex; align-items: center; justify-content: space-between; + gap: 10px; border-radius: 10px; border: 1px solid var(--dark); background: var(--white); - box-shadow: 1px 2px 3px 0 rgba(0, 0, 0, 0.25); + box-shadow: none; width: 100%; - height: 56px; + min-height: 56px; + padding: 10px 16px; box-sizing: border-box; } @@ -129,6 +139,8 @@ color: var(--gray600); display: flex; align-items: center; + align-self: center; + flex-shrink: 0; padding: 0; line-height: 1; } @@ -145,12 +157,18 @@ font-size: 24px; font-weight: 500; color: var(--black); + /* padding: 12px */ +} + +.longText { + font-size: 19px; } .understandCount { font-weight: 300; color: var(--black); font-size: 18px; + white-space: nowrap; } /* ── O/X 버튼 ── */ @@ -207,18 +225,35 @@ } .questionCard { - padding: 14px 16px; + padding: 16px 18px; cursor: pointer; transition: box-shadow 0.2s; - border-radius: 30px; + border-radius: 20px; background: var(--white); - box-shadow: 1px 2px 3px 0 rgba(0, 0, 0, 0.25); + box-shadow: + 0 1px 4px rgba(0,0,0,0.06), + 0 0 0 1px rgba(0,0,0,0.03); width: 95%; box-sizing: border-box; min-height: 40px; } +/* 해결된 질문: 미해결 질문과 한눈에 구분되도록 배경을 연한 회색으로 + 카드 전체를 흐리게 처리 (초록 테두리는 미해결 질문 전용이므로 제외) */ +.questionCardResolved { + background: var(--gray50); + opacity: 0.6; + border-color: transparent; + box-shadow: + 0 1px 4px rgba(0,0,0,0.06), + 0 0 0 1px rgba(0,0,0,0.03); +} + .questionCard:hover { + transform: translateY(-1px); + box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.15); +} + +.questionCardResolved:hover { box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.15); } @@ -620,7 +655,13 @@ } .understandName { - font-size: 20px; + font-size: 18px; + } + + .longText { + font-size: 17px; + margin: 15px; + } .understandCount { @@ -637,6 +678,40 @@ } } +/* ════════════════════════════════════════ + 반응형 — (600px 이하) +════════════════════════════════════════ */ +@media (max-width: 600px) { + .understandBar { + padding: 10px 12px; + gap: 4px; + } + + .understandName { + font-size: 18px; + margin: 6px + } + + .longText { + font-size: 15px; + margin: 8px + } + + .understandCount { + font-size: 15px; + } + + .oxBtn { + width: 32px; + height: 32px; + } + + .arrowBtn svg { + width: 24px; + height: 24px; + } +} + /* ════════════════════════════════════════ 반응형 — 모바일 (480px 이하) @@ -661,14 +736,18 @@ /* ── 이해도 바 ── */ .understandBar { - height: auto; min-height: 48px; padding: 8px 10px; - flex-wrap: wrap; } .understandName { - font-size: 17px; + font-size: 18px; + margin: 6px + } + + .longText { + font-size: 15px; + margin: 8px } .understandCount { @@ -676,8 +755,8 @@ } .oxBtn { - width: 30px; - height: 30px; + width: 29px; + height: 29px; font-size: 12px; margin: 0 2px; } diff --git a/frontend/src/pages/qna/QnAMainPage.js b/frontend/src/pages/qna/QnAMainPage.js index f799420..83b14db 100644 --- a/frontend/src/pages/qna/QnAMainPage.js +++ b/frontend/src/pages/qna/QnAMainPage.js @@ -44,69 +44,71 @@ function QNAMainPage() { if (error) return
오류: {error}
; return ( -
+
+
- {/* ── 진행 중인 세션 ── */} - {activeSessions.length > 0 && ( - <> -
-

현재 세션

- {activeSessions.map(session => ( -
navigate(`/sessions/${session.sessionId}/questions`, { state: { status: 'IN_SESSION' } })} - > -

- {getIcon(session.dayPart)} - {session.title} -

-

- {session.week}주차 {DAY_OF_WEEK_KO[session.dayOfWeek]} {DAY_PART_KO[session.dayPart]} -

-

{formatDate(session.sessionDate)}

-

{getTime(session.dayPart)}

-
- ))} -
-
- - )} + {/* ── 진행 중인 세션 ── */} + {activeSessions.length > 0 && ( + <> +
+

현재 세션

+ {activeSessions.map(session => ( +
navigate(`/sessions/${session.sessionId}/questions`, { state: { status: 'IN_SESSION' } })} + > +

+ {getIcon(session.dayPart)} + {session.title} +

+

+ {session.week}주차 {DAY_OF_WEEK_KO[session.dayOfWeek]} {DAY_PART_KO[session.dayPart]} +

+

{formatDate(session.sessionDate)}

+

{getTime(session.dayPart)}

+
+ ))} +
+
+ + )} - {/* ── 지난 세션 ── */} - {pastSessions.length > 0 && ( -
-

지난 세션

-
- {pastSessions.map(session => ( -
navigate(`/sessions/${session.sessionId}/questions`, { state: { status: 'AFTER_SESSION' } })} - > - - {getIcon(session.dayPart)} - {session.title} - -  • {session.week}주차 {DAY_OF_WEEK_KO[session.dayOfWeek]} {DAY_PART_KO[session.dayPart]} + {/* ── 지난 세션 ── */} + {pastSessions.length > 0 && ( +
+

지난 세션

+
+ {pastSessions.map(session => ( +
navigate(`/sessions/${session.sessionId}/questions`, { state: { status: 'AFTER_SESSION' } })} + > + + {getIcon(session.dayPart)} + {session.title} + +  • {session.week}주차 {DAY_OF_WEEK_KO[session.dayOfWeek]} {DAY_PART_KO[session.dayPart]} + - - -
- ))} -
-
- )} + +
+ ))} +
+
+ )} - {/* ── 세션 없을 때 ── */} - {activeSessions.length === 0 && pastSessions.length === 0 && ( -
-

아직 생성된 Q&A가 없어요

-
- )} + {/* ── 세션 없을 때 ── */} + {activeSessions.length === 0 && pastSessions.length === 0 && ( +
+

아직 생성된 Q&A가 없어요

+
+ )} +
); } diff --git a/frontend/src/pages/qna/QnAMainPage.module.css b/frontend/src/pages/qna/QnAMainPage.module.css index 38d5103..0d10f08 100644 --- a/frontend/src/pages/qna/QnAMainPage.module.css +++ b/frontend/src/pages/qna/QnAMainPage.module.css @@ -1,10 +1,17 @@ /* ── 페이지 레이아웃 ── */ +.pageWrapper { + min-height: 100vh; + background: #f2f2f0; + padding-top: 1px; +} + .page { min-height: 100vh; max-width: 880px; margin: 0 auto; padding: 0 16px; box-sizing: border-box; + background: #f2f2f0; } /* ── 섹션 공통 ── */ @@ -27,17 +34,32 @@ background: var(--white); border-radius: 10px; padding: 16px; + padding-bottom: 24px; text-align: center; width: 356px; height: 148px; - box-shadow: 2px 2px 5px 0 rgba(0, 0, 0, 0.25); + + border: 1px solid rgba(0,0,0,0.06); + + box-shadow: + 0 1px 0 rgba(255,255,255,0.8) inset, + 0 3px 8px rgba(0,0,0,0.10), + 0 1px 3px rgba(0,0,0,0.06); + + cursor: pointer; - transition: box-shadow 0.2s; + transition: + transform 0.2s ease, + box-shadow 0.2s ease; margin: 0 auto; + margin-bottom: 40px; } .card:hover { - box-shadow: 4px 4px 10px 0 rgba(0, 0, 0, 0.2); + box-shadow: + 0 1px 0 rgba(255,255,255,0.8) inset, + 0 6px 12px rgba(0,0,0,0.14), + 0 2px 5px rgba(0,0,0,0.08); } .card:hover .cardTitle { @@ -77,7 +99,16 @@ /* ── 지난 세션 목록 ── */ .icon { + display: inline-block; + position: relative; + top: -2px; + margin-right: 20px; + transition: color 0.2s; +} + +.enterIcon { + font-size: 20px; } .list { @@ -93,27 +124,30 @@ display: flex; align-items: center; justify-content: space-between; - box-shadow: 2px 2px 5px 0 rgba(0, 0, 0, 0.25); + box-shadow: 0 1px 4px rgba(0,0,0,0.06), + 0 0 0 1px rgba(0,0,0,0.03); cursor: pointer; transition: box-shadow 0.2s; } .listItem:hover { - box-shadow: 4px 4px 10px 0 rgba(0, 0, 0, 0.2); + box-shadow: 0 3px 7px rgba(0, 0, 0, 0.1); } .listTitle { font-family: var(--font-main); - font-size: 24px; + font-size: 23px; font-weight: 500; color: var(--black); + transition: color 0.2s; } .listWeek { font-family: var(--font-main); - font-size: 18px; + font-size: 15px; font-weight: 500; color: var(--gray600); + transition: color 0.2s; } .enterBtn { @@ -128,6 +162,10 @@ color: var(--dark); } +.listItem:hover .icon { + color: var(--dark); +} + /* ── 빈 상태 ── */ .empty { text-align: center; @@ -135,6 +173,12 @@ margin-top: 40px; } +.listItem:hover .enterBtn, +.listItem:hover .listTitle, +.listItem:hover .listWeek { + color: var(--dark); +} + /* ════════════════════════════════════════ 반응형 — 태블릿 (768px 이하) @@ -202,6 +246,7 @@ .card { width: min(356px, 80%); + min-height: 140px; padding: 14px 12px; } @@ -221,6 +266,10 @@ } .listItem { + width: 88%; + margin: 0 auto; + min-height: 40px; + padding: 12px; gap: 8px; } @@ -230,14 +279,24 @@ } .listWeek { - font-size: 13px; + font-size: 12px; } - .icon { - margin-right: 10px; + .cardTitle .icon { + margin-right: 6px; + } + + .listItem .icon { + margin-left: 8px; + margin-right: 12px; + margin-top: 2px; flex-shrink: 0; } + .enterIcon { + font-size: 18px; + } + .enterBtn { padding: 4px; } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..542d230 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,60 @@ +{ + "name": "piroin", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "framer-motion": "^12.40.0" + } + }, + "node_modules/framer-motion": { + "version": "12.40.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.40.0.tgz", + "integrity": "sha512-uaBd3qC1v3KQqBEjwTUd183K6PbS+j0yR9w9VmEOLWA/tnUcSn8Xa3uck7t4dgpDoUss8xQTcj8W2L07lrnLFg==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.40.0", + "motion-utils": "^12.39.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/motion-dom": { + "version": "12.40.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.40.0.tgz", + "integrity": "sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.39.0" + } + }, + "node_modules/motion-utils": { + "version": "12.39.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz", + "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..00f24cb --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "framer-motion": "^12.40.0" + } +}