diff --git a/docs/project/work.md b/docs/project/work.md index 426f3b8..2ec8928 100644 --- a/docs/project/work.md +++ b/docs/project/work.md @@ -1,5 +1,5 @@ - **슬로프캠 운영 환경 스트림 미출력 이슈 전수 조치 (2026-10-01)**: - - 상태: 구현, 빌드 검증, 마이그레이션 006 작성 완료 + - 상태: main 머지(ec22a61), 백엔드·프론트엔드 운영 배포 완료, 운영 실측 검증 완료 (200 OK) - 내용: - **비발디파크 404 해결**: `frontend/Dockerfile`의 runner 스테이지에 `COPY --from=builder /app/public ./public` 추가하여 운영 컨테이너에 `vivaldi.html` 및 `vivaldi.js` 정적 에셋이 포함되도록 수정. - **하이원·오투·무주 401 해결**: Nginx의 `/api/` ➔ 백엔드(8080) 라우팅 규칙과의 충돌을 피하기 위해 Next.js 스트림 프록시 라우트를 `/api/*-stream`에서 `/stream-proxy/*`(`high1`, `o2`, `muju`)로 분리 이동. @@ -14,6 +14,17 @@ - **빌드 검증**: - 프론트엔드 Turbopack 프로덕션 빌드(`npm run build`) 통과. - 백엔드 컴파일 및 `spotlessCheck` 통과. + - **운영 배포 및 실측 완료**: + - PR #32 (`ec22a61`) `main` 브랜치 머지 완료. + - `Deploy Backend` (36820657756): DB 006 마이그레이션 적용 및 백엔드 배포 성공. + - `Deploy Frontend` (36820657861): 프론트엔드 빌드 및 ECR 푸시, EC2 재기동 성공. + - 운영 환경(`https://snowthing.org`) 실측: + * `https://snowthing.org/vivaldi.html`: HTTP 200 OK (정상 서빙) + * `https://snowthing.org/stream-proxy/high1/1/playlist.m3u8`: HTTP 200 OK (`application/vnd.apple.mpegurl`) + * `https://snowthing.org/stream-proxy/o2/cam0.m3u8`: HTTP 200 OK (`application/vnd.apple.mpegurl`) + * `https://snowthing.org/stream-proxy/muju/01/playlist.m3u8`: HTTP 200 OK (`application/vnd.apple.mpegurl`) + * `https://snowthing.org/api/v1/resort-cams`: 휘닉스파크 7개소 최신 HLS URL 정상 응답 + * `https://snowthing.org/resort-cam`: 슬로프캠 뷰어 메인 페이지 200 OK 정상 서빙 - **슬로프캠 백엔드 CI 통과 및 CodeRabbit 피드백 전수 조치 (2026-09-29)**: - 상태: 구현, 테스트 통과, DB/시드/CI스크립트 동기화 완료 @@ -1831,3 +1842,43 @@ - `npm run build`로 리조트캠 전체·리조트별·개별 카메라 동적 라우트를 포함한 production build를 통과했습니다. - Docker MySQL 8 임시 DB에서 `003`·`004`·`005` 마이그레이션 최초 적용과 재실행 이력, 활성 리조트 12개, 카메라 89개, 4개 소스 유형을 검증했습니다. 검증용 DB는 완료 후 삭제했습니다. - 별도 검증 DB와 백엔드 포트에서 비로그인 `GET /api/v1/resort-cams`가 200으로 리조트 12개와 카메라 89개를 반환하는지 확인하고 검증 환경을 정리했습니다. + +## 운영 EC2 디스크 긴급 정리 + +- 일자: 2026-10-01 +- 루트 볼륨이 28GB 중 28GB를 사용해 사용률 99%, 가용 공간 287MB까지 감소한 상태를 확인했습니다. +- Docker 이미지 19.16GB 중 17.52GB, 빌드 캐시 5.519GB 중 4.563GB가 회수 가능한 상태였습니다. +- 실행 중인 컨테이너와 볼륨은 유지하고 `docker image prune -af`, `docker builder prune -af`로 미사용 이미지 17.23GB와 빌드 캐시 4.563GB를 정리했습니다. +- 정리 후 루트 볼륨 사용량은 7.6GB, 가용 공간은 21GB, 사용률은 27%로 회복했습니다. +- `snowthing-backend`, `snowthing-frontend` 컨테이너가 모두 실행 중인 상태를 확인했습니다. +- 현재 ECR 배포 스크립트에는 EC2 로컬 이미지 정리 단계가 없어 재발 가능성이 있습니다. 배포 성공 후 미사용 이미지를 정리하는 정책은 별도 변경 계획으로 반영해야 합니다. + +## 댓글 UI 정리 + +- 일자: 2026-10-01 +- 익명 게시판에서도 댓글 영역 제목을 `댓글`로 통일하고, 작성 폼의 익명 아이콘과 익명 표기를 제거했습니다. +- 비회원 댓글·답글 비밀번호 입력란에 명확한 라벨과 `비밀번호를 입력하세요` 안내를 적용했습니다. +- 비밀번호 누락 안내를 `비밀번호를 입력해주세요.`로 단순화했습니다. +- 댓글 작성 폼을 단일 입력 카드로 정리하고 비밀번호와 등록 버튼을 하단 작업 영역에 배치했습니다. +- 대댓글은 들여쓰기, 옅은 배경, 테두리와 답글 아이콘을 적용해 일반 댓글과 계층을 구분했습니다. +- 모바일에서는 비밀번호 입력과 등록 버튼이 세로로 배치되도록 반응형 스타일을 적용했습니다. +- 변경 대상 파일 ESLint와 프런트엔드 production build를 통과했습니다. 전체 ESLint에는 기존 리조트캠 미사용 변수 1건과 `public/vivaldi.js` 경고 43건이 남아 있습니다. + +## 중고장터 가격 옵션 UI 수정 + +- 일자: 2026-10-01 +- 가격 행의 공통 입력 스타일이 체크박스에도 적용되어 `가격 협의 가능`, `무료 나눔` 체크박스가 과도하게 커지던 문제를 수정했습니다. +- 가격 입력 스타일의 적용 대상을 숫자 입력란으로 제한하고 체크박스 크기를 `16px`로 고정했습니다. +- 옵션 문구가 불필요하게 줄바꿈되지 않도록 한 줄 정렬을 적용했습니다. +- 판매글 등록과 수정 화면이 같은 스타일을 공유하므로 두 화면에 동일하게 반영됩니다. + +## EC2 Docker 이미지 자동 정리 + +- 일자: 2026-10-01 +- 배포 성공과 헬스체크 완료 후 해당 서비스의 오래된 ECR 로컬 이미지를 자동으로 정리하도록 배포 스크립트를 수정했습니다. +- 서비스별 현재 실행 이미지와 직전 배포 이미지 1개를 image ID 기준으로 보호해 즉시 롤백할 수 있도록 했습니다. +- 다른 서비스의 롤백 이미지를 제거할 수 있는 전역 `docker image prune -af` 대신, 현재 배포 대상 ECR 저장소의 오래된 digest만 선별 삭제합니다. +- 사용하지 않는 Docker 빌드 캐시는 배포 성공 후 정리합니다. +- 정리 실패는 경고로 기록하되 이미 헬스체크를 통과한 배포를 실패나 롤백 상태로 바꾸지 않습니다. +- 정리 전후 `docker system df` 결과와 삭제 개수, 보호한 이미지 ID를 배포 로그에 남깁니다. +- Bash 문법 검사와 ShellCheck를 수행했으며, 새 정리 로직에서는 오류가 발견되지 않았습니다. ShellCheck 정보 메시지 4건은 기존 Nginx 설정에 `$` 리터럴을 전달하기 위한 의도된 작은따옴표 사용입니다. diff --git a/frontend/app/globals.css b/frontend/app/globals.css index 3c58ba1..18cc54f 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -554,11 +554,42 @@ button:disabled { .post-detail-card > h1 { margin: 12px 0; color: #036e9f; font-size: clamp(22px,3vw,30px); font-weight: 850; letter-spacing: -.04em; line-height: 1.3; }.post-detail-author { display: flex; align-items: center; gap: 10px; border-radius: 10px; background: #f1f3f5; padding: 11px 12px; }.post-detail-author > .material-symbols-outlined { display: grid; width: 32px; height: 32px; place-items: center; border-radius: 50%; background: #0284c7; color: white; font-size: 18px; }.post-detail-author > div:not(.post-detail-owner-actions) { display: flex; flex-direction: column; gap: 2px; }.post-detail-author strong { color: #475569; font-size: 11px; }.post-detail-author small { color: #94a3b8; font-size: 9px; }.post-detail-owner-actions { display: flex; align-items: center; gap: 10px; margin-left: auto; }.post-detail-owner-actions a,.post-detail-owner-actions button { border: 0; background: none; color: #64748b; font-size: 10px; font-weight: 750; }.post-detail-owner-actions button { color: #e11d48; } .post-detail-content { min-height: 220px; padding: 28px 2px; color: #334155; font-size: 14px; line-height: 1.8; }.post-detail-images { display: grid; gap: 12px; }.post-detail-images img { width: 100%; max-height: 720px; border-radius: 10px; object-fit: contain; }.post-detail-reactions { display: flex; flex-direction: column; align-items: center; gap: 9px; border-top: 1px solid #f1f5f9; padding-top: 20px; }.post-detail-reactions > div { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; }.post-detail-reactions button { display: inline-flex; min-height: 40px; align-items: center; gap: 6px; border: 0; border-radius: 10px; background: #f1f3f5; padding: 0 13px; color: #64748b; font-size: 11px; font-weight: 750; }.post-detail-reactions button.active { background: #0284c7; color: white; }.post-detail-reactions .material-symbols-outlined { font-size: 16px; }.post-detail-reactions p { margin: 0; } .post-comments-card { padding: 22px 24px; }.post-comments-card > header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 18px; }.post-comments-card > header h2 { margin: 0; color: #036e9f; font-size: 15px; font-weight: 850; }.post-comments-card > header h2 span { display: inline-flex; min-width: 24px; height: 20px; align-items: center; justify-content: center; border-radius: 99px; background: #e0e7ff; color: #64748b; font-family: Inter,sans-serif; font-size: 10px; } -.post-comment-compose { border-radius: 11px; background: #f1f3f5; padding: 13px; }.post-comment-identity { display: flex; align-items: center; gap: 8px; margin-bottom: 9px; }.post-comment-identity > span { display: inline-flex; height: 25px; align-items: center; gap: 4px; border-radius: 7px; background: white; padding: 0 8px; color: #036e9f; font-size: 10px; font-weight: 800; }.post-comment-identity .material-symbols-outlined { font-size: 14px; }.post-comment-identity input { width: 105px; height: 25px; border: 0; border-radius: 7px; outline: 0; padding: 0 8px; font-size: 10px; }.post-comment-textarea { width: 100%; min-height: 95px; resize: vertical; border: 0; border-radius: 9px; outline: 0; background: white; padding: 12px; color: #334155; font-size: 12px; }.post-comment-textarea:focus { box-shadow: 0 0 0 2px #0891b2; }.post-comment-submit-row { display: flex; justify-content: flex-end; margin-top: 9px; }.post-comment-submit-row button { display: inline-flex; min-height: 36px; align-items: center; gap: 5px; border: 0; border-radius: 8px; background: #0284c7; padding: 0 14px; color: white; font-size: 11px; font-weight: 800; }.post-comment-submit-row .material-symbols-outlined { font-size: 15px; }.post-comment-login-note { display: block; margin-top: 7px; color: #94a3b8; font-size: 10px; }.post-comment-list { display: grid; gap: 15px; margin-top: 20px; }.post-comments-more { display: grid; justify-items: center; gap: 6px; margin-top: 20px; }.post-comments-more-button { min-height: 38px; border: 0; border-radius: 9px; background: #f1f3f5; padding: 0 18px; color: #64748b; font-size: 11px; font-weight: 750; } +.post-comment-compose { overflow: hidden; border: 1px solid #dbe3ed; border-radius: 10px; background: white; } +.post-comment-textarea { display: block; width: 100%; min-height: 104px; resize: vertical; border: 0; outline: 0; background: transparent; padding: 14px 15px; color: #334155; font-size: 13px; line-height: 1.65; } +.post-comment-textarea::placeholder { color: #94a3b8; } +.post-comment-compose:focus-within,.post-reply-compose:focus-within { border-color: #7dd3fc; box-shadow: 0 0 0 3px rgba(14,165,233,.08); } +.post-comment-compose-footer { display: flex; min-height: 52px; align-items: center; justify-content: flex-end; gap: 10px; border-top: 1px solid #e2e8f0; background: #f8fafc; padding: 8px 9px 8px 13px; } +.post-comment-password { display: flex; min-width: 0; align-items: center; gap: 9px; margin-right: auto; color: #64748b; font-size: 11px; font-weight: 700; } +.post-comment-password input { width: 180px; height: 34px; border: 1px solid #dbe3ed; border-radius: 7px; outline: 0; background: white; padding: 0 10px; color: #334155; font-size: 11px; } +.post-comment-password input:focus { border-color: #38bdf8; } +.post-comment-submit-button { display: inline-flex; min-height: 36px; flex: 0 0 auto; align-items: center; justify-content: center; gap: 5px; border: 0; border-radius: 7px; background: #0f2942; padding: 0 14px; color: white; font-size: 11px; font-weight: 800; } +.post-comment-submit-button:hover { background: #0369a1; } +.post-comment-submit-button .material-symbols-outlined { color: #7dd3fc; font-size: 15px; } +.post-comment-login-note { display: block; margin: 7px 12px; color: #94a3b8; font-size: 10px; } +.post-comment-list { display: grid; margin-top: 22px; } +.post-comment-item { border-bottom: 1px solid #e2e8f0; padding: 0 0 18px; } +.post-comment-item + .post-comment-item { padding-top: 18px; } +.post-comment-reply { margin: 12px 0 0 24px; border: 1px solid #e2e8f0; border-radius: 9px; background: #f8fafc; padding: 14px 16px; } +.post-comment-reply-author { display: flex; min-width: 0; align-items: center; gap: 6px; } +.post-comment-reply-author > .material-symbols-outlined { color: #94a3b8; font-size: 17px; } +.post-reply-compose { overflow: hidden; margin: 14px 0 0 24px; border: 1px solid #dbe3ed; border-radius: 9px; background: white; } +.post-reply-compose .post-comment-textarea { min-height: 82px; } +.post-comments-more { display: grid; justify-items: center; gap: 6px; margin-top: 20px; }.post-comments-more-button { min-height: 38px; border: 0; border-radius: 9px; background: #f1f3f5; padding: 0 18px; color: #64748b; font-size: 11px; font-weight: 750; } .post-detail-sidebar { display: flex; flex-direction: column; gap: 6px; }.post-detail-sidebar > p { margin: 0; padding-inline: 5px; color: #94a3b8; font-size: 9px; }.detail-carpool-card { padding: 15px; }.detail-carpool-card > header { display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid #f1f5f9; padding-bottom: 10px; }.detail-carpool-card h2 { display: flex; align-items: center; gap: 5px; margin: 0; color: #036e9f; font-size: 12px; }.detail-carpool-card h2 .material-symbols-outlined { font-size: 16px; }.detail-carpool-card header small { color: #94a3b8; font-size: 9px; }.detail-carpool-card article { border-bottom: 1px solid #f1f5f9; padding-block: 12px; }.detail-carpool-card article:last-child { border-bottom: 0; padding-bottom: 2px; }.detail-carpool-card article div,.detail-carpool-card article p { display: flex; align-items: center; justify-content: space-between; gap: 8px; }.detail-carpool-card article div b { border-radius: 4px; background: #475569; padding: 3px 6px; color: white; font-size: 8px; }.detail-carpool-card time { color: #94a3b8; font-size: 9px; }.detail-carpool-card article > strong { display: block; margin-top: 8px; color: #036e9f; font-size: 10px; }.detail-carpool-card article > strong span { color: #94a3b8; }.detail-carpool-card article p { margin: 6px 0 0; color: #94a3b8; font-size: 9px; }.detail-carpool-card article p b { color: #334155; } @media (max-width: 960px) { .post-detail-layout { grid-template-columns: 1fr; }.post-detail-sidebar { order: 2; } } -@media (max-width: 640px) { .post-detail-page { padding-block: 12px 32px; }.post-detail-card,.post-comments-card { padding: 17px 14px; }.post-detail-card > h1 { font-size: 21px; }.post-detail-meta-top > div { gap: 3px; }.post-detail-content { min-height: 160px; padding-block: 22px; }.post-comment-compose { padding: 10px; } } +@media (max-width: 640px) { + .post-detail-page { padding-block: 12px 32px; } + .post-detail-card,.post-comments-card { padding: 17px 14px; } + .post-detail-card > h1 { font-size: 21px; } + .post-detail-meta-top > div { gap: 3px; } + .post-detail-content { min-height: 160px; padding-block: 22px; } + .post-comment-compose-footer { align-items: stretch; flex-direction: column; } + .post-comment-password { width: 100%; } + .post-comment-password input { min-width: 0; flex: 1; width: auto; } + .post-comment-submit-button { width: 100%; } + .post-comment-reply,.post-reply-compose { margin-left: 12px; } +} /* Shared board list */ .board-page { min-height: calc(100vh - 142px); padding-block: 12px 48px; } @@ -898,14 +929,25 @@ html[data-theme="dark"] .pagination button.current { html[data-theme="dark"] .compose-title-input, html[data-theme="dark"] .compose-image-input, html[data-theme="dark"] .compose-password-field input, -html[data-theme="dark"] .post-comment-identity > span, -html[data-theme="dark"] .post-comment-identity input, +html[data-theme="dark"] .post-comment-password input, html[data-theme="dark"] .post-comment-textarea { border-color: var(--snow-border); background: var(--snow-surface-mid); color: var(--snow-ink); } +html[data-theme="dark"] .post-comment-compose, +html[data-theme="dark"] .post-reply-compose, +html[data-theme="dark"] .post-comment-reply, +html[data-theme="dark"] .post-comment-compose-footer { + border-color: var(--snow-border); + background: var(--snow-surface-low); +} + +html[data-theme="dark"] .post-comment-item { + border-color: var(--snow-border); +} + html[data-theme="dark"] .compose-title-input:focus, html[data-theme="dark"] .compose-image-input:focus, html[data-theme="dark"] .compose-password-field input:focus { @@ -979,7 +1021,7 @@ html[data-theme="dark"] .board-write-button, html[data-theme="dark"] .board-category-tabs button.active, html[data-theme="dark"] .board-pagination button.current, html[data-theme="dark"] .post-detail-reactions button.active, -html[data-theme="dark"] .post-comment-submit-row button { +html[data-theme="dark"] .post-comment-submit-button { border-color: #1e5b84; background: #1e5b84; color: white; @@ -1174,8 +1216,8 @@ html[data-theme="dark"] .market-results.album .market-result-card { border-color .market-compose-card { display: flex; flex-direction: column; gap: 22px; border: 1px solid var(--snow-border); border-radius: 13px; background: var(--snow-canvas); padding: 26px; box-shadow: var(--community-card-shadow); } .market-form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } .market-form-grid label,.market-form-field,.market-price-row > label:first-child { display: flex; flex-direction: column; gap: 7px; color: var(--snow-ink); font-size: 12px; font-weight: 700; } -.market-form-grid select,.market-form-field input,.market-price-row input { height: 44px; border: 1px solid var(--snow-border-strong); border-radius: 8px; outline: 0; background: var(--snow-background); padding: 0 12px; color: var(--snow-ink); font: inherit; font-weight: 500; } -.market-form-grid select:focus,.market-form-field input:focus,.market-price-row input:focus { border-color: var(--alpine-sky); box-shadow: 0 0 0 3px rgba(2,132,199,.1); } +.market-form-grid select,.market-form-field input,.market-price-row > label:first-child input { height: 44px; border: 1px solid var(--snow-border-strong); border-radius: 8px; outline: 0; background: var(--snow-background); padding: 0 12px; color: var(--snow-ink); font: inherit; font-weight: 500; } +.market-form-grid select:focus,.market-form-field input:focus,.market-price-row > label:first-child input:focus { border-color: var(--alpine-sky); box-shadow: 0 0 0 3px rgba(2,132,199,.1); } .market-form-field small,.market-editor-field p,.market-image-field small { color: var(--snow-muted); font-weight: 500; } .market-radio-field { display: flex; align-items: center; gap: 20px; border: 0; padding: 0; } .market-radio-field legend { float: left; min-width: 82px; color: var(--snow-ink); font-size: 12px; font-weight: 700; } @@ -1184,9 +1226,10 @@ html[data-theme="dark"] .market-results.album .market-result-card { border-color .market-price-row { display: flex; align-items: flex-end; gap: 18px; } .market-price-row > label:first-child { flex: 1; } .market-price-row > label:first-child > div { position: relative; } -.market-price-row input { width: 100%; padding-right: 38px; } +.market-price-row > label:first-child input { width: 100%; padding-right: 38px; } .market-price-row b { position: absolute; top: 50%; right: 13px; color: var(--snow-muted); font-size: 12px; transform: translateY(-50%); } -.market-check { height: 44px; } +.market-check { height: 44px; cursor: pointer; white-space: nowrap; } +.market-check input { flex: 0 0 16px; width: 16px; height: 16px; margin: 0; padding: 0; cursor: pointer; } .market-editor-field > span,.market-image-field > div > span { color: var(--snow-ink); font-size: 12px; font-weight: 700; } .market-editor-field p { margin: 5px 0 9px; font-size: 11px; } .market-image-field > div { display: flex; justify-content: space-between; margin-bottom: 8px; } diff --git a/frontend/app/posts/[publicId]/page.tsx b/frontend/app/posts/[publicId]/page.tsx index 231a241..1ee664e 100644 --- a/frontend/app/posts/[publicId]/page.tsx +++ b/frontend/app/posts/[publicId]/page.tsx @@ -371,7 +371,7 @@ export default function PostDetailPage({ params }: { params: Promise<{ publicId: if (!currentUserPublicId) { const pwd = parentId ? replyAnonPassword : commentAnonPassword; if (!pwd.trim()) { - alert("익명 댓글 삭제를 위한 비밀번호를 입력해주세요."); + alert("비밀번호를 입력해주세요."); return; } anonymousPassword = pwd.trim(); @@ -648,25 +648,30 @@ export default function PostDetailPage({ params }: { params: Promise<{ publicId:
-

{isAnonymousPost ? "익명 댓글" : "댓글"} {totalCommentCount}

+

댓글 {totalCommentCount}

- {isAnonymousPost && ( -
- theater_comedy익명 - {!currentUserPublicId && setCommentAnonPassword(event.target.value)} />} -
- )}