From e2eee3ed96ddbd2c98ff801b8148f020bedf7942 Mon Sep 17 00:00:00 2001 From: xihxxn Date: Wed, 1 Jul 2026 15:29:45 +0900 Subject: [PATCH 1/7] =?UTF-8?q?fix:=20=EB=B0=B0=ED=8F=AC=20=EC=8B=9C=20?= =?UTF-8?q?=EB=AF=B8=EC=82=AC=EC=9A=A9=20Docker=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=A0=95=EB=A6=AC=20=EB=B0=8F=20=ED=97=AC=EC=8A=A4?= =?UTF-8?q?=EC=B2=B4=ED=81=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 21f1f7b..75e9f92 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -59,7 +59,7 @@ jobs: --restart unless-stopped \ -p 8080:8080 \ -v piroin-uploads:/app/uploads \ - --health-cmd="curl -f http://localhost:8080/actuator/health || exit 1" \ + --health-cmd="wget -q -O /dev/null http://localhost:8080/actuator/health || exit 1" \ --health-interval=30s \ --health-timeout=10s \ --health-retries=3 \ @@ -70,3 +70,4 @@ jobs: -e JWT_SECRET="${{ secrets.JWT_SECRET }}" \ -e JWT_EXPIRATION="${{ secrets.JWT_EXPIRATION }}" \ ${{ secrets.DOCKERHUB_USERNAME }}/piroin-backend:latest + docker image prune -a -f From 99ae345024790febfcde01137f587a6422435cfb Mon Sep 17 00:00:00 2001 From: xihxxn Date: Wed, 1 Jul 2026 15:57:29 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20=EB=B0=B0=ED=8F=AC=20=EC=8B=9C=20Do?= =?UTF-8?q?cker=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20SHA=20=ED=83=9C=EA=B7=B8?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 75e9f92..6c94259 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -41,8 +41,10 @@ jobs: - name: Build and push Docker image working-directory: backend run: | - docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/piroin-backend:latest . + docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/piroin-backend:latest \ + -t ${{ secrets.DOCKERHUB_USERNAME }}/piroin-backend:${{ github.sha }} . docker push ${{ secrets.DOCKERHUB_USERNAME }}/piroin-backend:latest + docker push ${{ secrets.DOCKERHUB_USERNAME }}/piroin-backend:${{ github.sha }} - name: Deploy to EC2 uses: appleboy/ssh-action@v1.0.0 From 8f3006bc2bf1120ac2b2091c0436579c1494daae Mon Sep 17 00:00:00 2001 From: issuejong Date: Fri, 10 Jul 2026 16:24:49 +0900 Subject: [PATCH 3/7] =?UTF-8?q?feat:=20=EB=B3=B8=EC=9D=B8=20=EC=A7=88?= =?UTF-8?q?=EB=AC=B8=EB=A7=8C=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/qna/QnAListPage.js | 150 ++++++++---------- frontend/src/pages/qna/QnAListPage.module.css | 59 ++++++- 2 files changed, 125 insertions(+), 84 deletions(-) diff --git a/frontend/src/pages/qna/QnAListPage.js b/frontend/src/pages/qna/QnAListPage.js index ef4ec8c..47cfe62 100644 --- a/frontend/src/pages/qna/QnAListPage.js +++ b/frontend/src/pages/qna/QnAListPage.js @@ -132,20 +132,6 @@ const updateQuestionGroupsByCheckedEvent = (groups, eventData) => { return hasUpdatedQuestion ? regroupQuestions(updatedQuestions) : groups; }; -const addQuestionToGroups = (groups, question) => { - if (!question?.questionId) return groups; - - const existingQuestions = [ - ...groups.popularQuestions, - ...groups.unresolvedQuestions, - ...groups.resolvedQuestions, - ]; - const alreadyExists = existingQuestions.some(item => item.questionId === question.questionId); - - if (alreadyExists) return groups; - return regroupQuestions([question, ...existingQuestions]); -}; - const buildUnderstandingCheckFromEvent = (eventData) => ({ checkId: eventData.checkId, content: eventData.content, @@ -199,6 +185,7 @@ function QnAListPage() { }); // ── 필터 / 정렬 상태 ───────────────────────────── + const [questionScope, setQuestionScope] = useState('all'); const [filterCurious, setFilterCurious] = useState(false); const [filterUnsolved, setFilterUnsolved] = useState(false); const [sortOrder, setSortOrder] = useState('정렬'); @@ -311,47 +298,13 @@ function QnAListPage() { applyQuestionGroups(nextGroups); }, [applyQuestionGroups]); - const buildQuestionFromCreatedEvent = useCallback(async (eventData) => { - if (!eventData?.questionId) return null; - - // SSE 이벤트의 imageUrls 배열을 blob URL로 변환 - const rawUrls = eventData.imageUrls ?? []; - const blobUrls = await Promise.all( - rawUrls.map(async (url) => { - try { - const imgRes = await authFetch(url); - const blob = await imgRes.blob(); - return URL.createObjectURL(blob); - } catch { - return null; - } - }) - ); + const handleQuestionCreatedEvent = useCallback((eventData) => { + if (!eventData?.questionId) return; - return { - questionId: eventData.questionId, - content: eventData.content, - imageUrls: blobUrls.filter(Boolean), - isResolved: false, - isPopular: false, - isLiked: false, - isMine: false, - isNew: eventData.isNew ?? true, - iLiked: false, - likeCount: eventData.likeCount ?? 0, - commentCount: eventData.commentCount ?? 0, - previewComments: [], - createdAt: eventData.createdAt, - }; - }, []); - - const handleQuestionCreatedEvent = useCallback(async (eventData) => { - const createdQuestion = await buildQuestionFromCreatedEvent(eventData); - if (!createdQuestion) return; - - const nextGroups = addQuestionToGroups(questionGroupsRef.current, createdQuestion); - applyQuestionGroups(nextGroups); - }, [applyQuestionGroups, buildQuestionFromCreatedEvent]); + // 생성 이벤트에는 로그인 사용자 기준 isMine 값이 없으므로, + // 목록을 다시 조회해 "내 질문" 필터에서도 정확히 분류되도록 한다. + void fetchQuestions(understandingIndex); + }, [fetchQuestions, understandingIndex]); const handleQuestionUpdatedEvent = useCallback((eventData) => { const nextGroups = updateQuestionGroupsByQuestionEvent(questionGroupsRef.current, eventData); @@ -706,8 +659,9 @@ function QnAListPage() { const displayedQuestions = (() => { let list = allQuestions; - if (isStaff && filterUnsolved) list = unresolvedQuestions; - if (!isStaff && filterCurious) list = allQuestions.filter(q => q.iLiked); + if (questionScope === 'mine') list = list.filter(q => q.isMine); + if (isStaff && filterUnsolved) list = list.filter(q => !q.isResolved); + if (!isStaff && filterCurious) list = list.filter(q => q.iLiked); if (sortOrder === '최신순') { list = [...list].sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); @@ -730,35 +684,56 @@ function QnAListPage() { {/* ── 필터 / 정렬 행 ── */}
- {isStaff ? ( - - ) : ( - - )} -
- - {showSortMenu && ( -
    - {['기본', '최신순', '저도궁금해요순'].map(option => ( -
  • { setSortOrder(option); setShowSortMenu(false); }}> - {option} -
  • - ))} -
+ +
+ +
+ {isStaff ? ( + + ) : ( + )} +
+ + {showSortMenu && ( +
    + {['기본', '최신순', '저도궁금해요순'].map(option => ( +
  • { setSortOrder(option); setShowSortMenu(false); }}> + {option} +
  • + ))} +
+ )} +

@@ -802,6 +777,15 @@ function QnAListPage() { {/* ── 질문 목록 ── */}
+ {displayedQuestions.length === 0 && ( +
+ {questionScope === 'mine' + ? '작성한 질문이 없습니다.' + : (filterCurious || filterUnsolved) + ? '조건에 맞는 질문이 없습니다.' + : '등록된 질문이 없습니다.'} +
+ )} {displayedQuestions.map(question => (
Date: Sat, 11 Jul 2026 14:54:34 +0900 Subject: [PATCH 4/7] =?UTF-8?q?[Fix]=20QnA=20List=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EA=B8=80=EC=9E=90=20=EB=9B=B0=EC=B3=90=EB=82=98=EA=B0=80?= =?UTF-8?q?=EB=8A=94=20=EA=B1=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/qna/QnAListPage.js | 3 ++- frontend/src/pages/qna/QnAListPage.module.css | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/qna/QnAListPage.js b/frontend/src/pages/qna/QnAListPage.js index 47cfe62..7ca7fbf 100644 --- a/frontend/src/pages/qna/QnAListPage.js +++ b/frontend/src/pages/qna/QnAListPage.js @@ -842,7 +842,8 @@ function QnAListPage() {
- {comment.content} + + {comment.content}
{comment.hasImage && (
Date: Sat, 11 Jul 2026 14:58:47 +0900 Subject: [PATCH 5/7] =?UTF-8?q?[Fix]=20QnA=20=EC=84=B8=EB=B6=80=20?= =?UTF-8?q?=EB=94=94=EC=9E=90=EC=9D=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/qna/QnAListPage.module.css | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/pages/qna/QnAListPage.module.css b/frontend/src/pages/qna/QnAListPage.module.css index e8165e3..37118b4 100644 --- a/frontend/src/pages/qna/QnAListPage.module.css +++ b/frontend/src/pages/qna/QnAListPage.module.css @@ -63,9 +63,9 @@ } .scopeTabActive { - background: var(--main); - color: var(--black); - font-weight: 700; + background: var(--dark); + color: var(--white); + font-weight: 650; } .filterControls { @@ -319,7 +319,7 @@ .qIcon { flex-shrink: 0; padding-top: 1px; - color: var(--main); + color: var(--dark); font-family: var(--font-main); font-size: 36px; font-weight: 900; @@ -358,7 +358,7 @@ color: var(--white); font-family: var(--font-main); font-size: 12px; - font-weight: 700; + font-weight: 650; letter-spacing: 0; } From 61bd462d56bce0749e5ff9c833a8ff49d0b64636 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Sat, 11 Jul 2026 16:08:59 +0900 Subject: [PATCH 6/7] =?UTF-8?q?[Fix]=20=EC=9E=85=EB=A0=A5=EC=B0=BD=20?= =?UTF-8?q?=EC=9E=85=EB=A0=A5=20=EA=B8=B8=EC=9D=B4=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=A5=B8=20=20=EC=9E=90=EB=8F=99=20=EC=A4=84=EB=B0=94=EA=BF=88?= =?UTF-8?q?=20=EB=B0=8F=20textarea=20=EB=84=93=EC=9D=B4=20=EC=A1=B0?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/qna/QnADetailPage.js | 19 +++++-- .../src/pages/qna/QnADetailPage.module.css | 12 ++++- frontend/src/pages/qna/QnAListPage.js | 38 +++++++++++--- frontend/src/pages/qna/QnAListPage.module.css | 49 ++++++++++++++----- 4 files changed, 95 insertions(+), 23 deletions(-) diff --git a/frontend/src/pages/qna/QnADetailPage.js b/frontend/src/pages/qna/QnADetailPage.js index f2b3a9f..bef7cfb 100644 --- a/frontend/src/pages/qna/QnADetailPage.js +++ b/frontend/src/pages/qna/QnADetailPage.js @@ -92,6 +92,7 @@ function QnADetailPage() { const [selectedImages, setSelectedImages] = useState([]); // 여러 장 const [imagePreviews, setImagePreviews] = useState([]); // 여러 장 미리보기 const fileInputRef = useRef(null); + const commentTextareaRef = useRef(null); // ── 댓글 수정 상태 ─────────────────────────────── const [commentMenuId, setCommentMenuId] = useState(null); @@ -420,6 +421,7 @@ function QnADetailPage() { setImagePreviews([]); // 로컬 상태에 blob URL을 직접 넣으면 새로고침 시 이미지가 깨지므로 // 등록 직후 fetchQuestion으로 서버의 정식 URL을 받아온다. + if (commentTextareaRef.current) commentTextareaRef.current.style.height = 'auto'; await fetchQuestion(); } } catch (err) { @@ -709,14 +711,25 @@ function QnADetailPage() { style={{ display: 'none' }} onChange={handleImageSelect} /> - setCommentText(e.target.value)} - onKeyDown={e => { if (e.key === 'Enter' && !e.nativeEvent.isComposing) handleCommentSubmit(); }} + onChange={e => { + setCommentText(e.target.value); + e.target.style.height = 'auto'; + e.target.style.height = `${Math.min(e.target.scrollHeight, 150)}px`; + }} + onKeyDown={e => { + if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) { + e.preventDefault(); + handleCommentSubmit(); + } + }} onPaste={handlePaste} + rows={1} disabled={isSubmitting} /> - { commentTextareaRefs.current[question.questionId] = el; }} className={styles.commentInput} placeholder="댓글을 입력해주세요..." value={commentInputs[question.questionId] || ''} - onChange={e => handleCommentChange(question.questionId, e.target.value)} - onKeyDown={e => { if (e.key === 'Enter' && !e.nativeEvent.isComposing) handleCommentSubmit(e, question.questionId); }} + onChange={e => { + handleCommentChange(question.questionId, e.target.value); + e.target.style.height = 'auto'; + e.target.style.height = `${Math.min(e.target.scrollHeight, 120)}px`; + }} + onKeyDown={e => { + if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) { + e.preventDefault(); + handleCommentSubmit(e, question.questionId); + } + }} onPaste={e => handleCommentPaste(e, question.questionId)} + rows={1} autoFocus />