From 5e0f76b7c106b917580bbde847ffce6e88c1c795 Mon Sep 17 00:00:00 2001 From: art Date: Tue, 30 Jun 2026 11:53:15 +0900 Subject: [PATCH] =?UTF-8?q?docs(analysis):=20JSP=20=ED=94=84=EB=A1=A0?= =?UTF-8?q?=ED=8A=B8=EC=97=94=EB=93=9C=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EC=A4=91=EB=B3=B5=C2=B7=EC=9C=A0=EC=A7=80=EB=B3=B4?= =?UTF-8?q?=EC=88=98=20=EA=B2=80=ED=86=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CSS 디자인 토큰 23개 파일 중복, JS 유틸 8중복, recruit-form CSRF 폴백 갭, 날짜 함수 편재 발견 - 권고: global.css 중앙화(P0), bibimbap-utils.js(P1), recruit-form CSRF 수정(P0-보안), bibimbap-date.js(P2) - work-session 아티팩트: research + design recommendations 포함 Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01K3FeMrbtxfTScjrwUukyHD --- .../frontend-design-recommendations.md | 542 ++++++++++++++++++ .../20260630-113258/documentation.md | 32 ++ .atp/work-session/20260630-113258/report.md | 48 ++ .../research/frontend-analysis.md | 268 +++++++++ .../2026-06-30-frontend-component-review.md | 261 +++++++++ docs/analysis/index.md | 1 + 6 files changed, 1152 insertions(+) create mode 100644 .atp/work-session/20260630-113258/artifacts/frontend-design-recommendations.md create mode 100644 .atp/work-session/20260630-113258/documentation.md create mode 100644 .atp/work-session/20260630-113258/report.md create mode 100644 .atp/work-session/20260630-113258/research/frontend-analysis.md create mode 100644 docs/analysis/2026-06-30-frontend-component-review.md diff --git a/.atp/work-session/20260630-113258/artifacts/frontend-design-recommendations.md b/.atp/work-session/20260630-113258/artifacts/frontend-design-recommendations.md new file mode 100644 index 0000000..c25f881 --- /dev/null +++ b/.atp/work-session/20260630-113258/artifacts/frontend-design-recommendations.md @@ -0,0 +1,542 @@ +--- +phase: design +agent: design-advisor +agent_version: 1 +generated_at: 2026-06-30T03:15:00Z +concerns: + - "admin-console.jsp, admin-jam-list.jsp JS 내부의 post() 함수 구현 세부가 admin-post-categories / admin-unity-feeds와 정확히 동일한지 코드 레벨 비교 필요. 통합 시 파라미터 처리 분기가 예상치 않게 동작 변경될 수 있음. 구현 advisor가 통합 전 3-way diff 수행 권고." + - "header.jsp 내 .site-header CSS 변수 블록(--header-bg 등)은 global.css 의 토큰 범위와 겹치지 않는 컴포넌트 전용 변수이므로 중앙화 대상에서 제외. 구현 시 헷갈리지 않도록 명칭 분리 유지 확인 요망." +concerns_checked: true +references: + requirements: "(인라인 입력 — 이 문서의 ## 맥락 섹션)" + research: ".atp/work-session/20260630-113258/research/frontend-analysis.md" + adrs: [] +--- + +# 프론트엔드 컴포넌트 개선 권고안 + +## 목표 / 비목표 + +### 목표 +- FR-1: CSS 디자인 토큰 중복 제거 → 23개 파일 대신 단일 파일 변경으로 전역 반영 +- FR-2: JS 공용 유틸리티(notify/openModal/post/handleResult/handleError) 중복 선언 제거 +- FR-3: BibimbapModal 호출 패턴을 단일 방식으로 표준화 +- FR-4: recruit-form의 CSRF 토큰 폴백 갭 수정 (보안) +- FR-5: 날짜 포맷 함수를 공용 파일로 추출하여 복사 중복 방지 + +### 비목표 +- Webpack/Vite 등 빌드 도구 도입 (별도 의사결정 사항) +- CSS-in-JS, PostCSS 파이프라인 도입 +- JSP 템플릿 엔진 교체 또는 React/Vue 마이그레이션 +- 기존 HTML/JSP 구조 및 클래스명 리팩터링 (CSS 토큰만 외부화) +- header.jsp·footer.jsp·modal.jsp 기존 구현 변경 +- 서버사이드 날짜 포매팅 도입 + +--- + +## 요약 (우선순위 매트릭스) + +| 이슈 | 영향도 | 구현 비용 | 우선순위 | +|---|---|---|---| +| CSS 토큰 23개 파일 중복 | 높음 (유지보수 23배 비용) | 낮음 (파일 추가 + include 삽입) | P0 | +| 폼 제출 CSRF 갭 (recruit-form) | 높음 (보안) | 낮음 (10줄 이내 수정) | P0 — 보안 | +| JS 공용 유틸리티 분리 | 중간 (동작 불일치 방지) | 중간 (파일 작성 + 기존 인라인 제거) | P1 | +| 모달 패턴 표준화 | 중간 (신규 페이지 기준 제공) | 중간 (패턴 A/B 대체 + admin 2곳 수정) | P1 | +| 날짜 포맷 유틸리티 모듈 | 낮음 (예방적 분리) | 낮음 (파일 추출) | P2 | + +--- + +## 권고 1 — CSS 토큰 중앙화 (P0) + +### 접근 + +`src/main/webapp/css/global.css` 정적 파일을 신규 생성한다. 이 파일에 라이트/다크 테마 CSS 변수 블록, `body` 기본 스타일, `safe-area` padding 믹스인 클래스, `.admin-btn` 컴포넌트를 선언한다. + +각 JSP의 `` 최상단에 `` 를 삽입한다. 이 link는 `theme-init.jsp` `` 직후에 위치시켜 FOUC(Flash of Unstyled Content)를 방지한다. + +Spring Boot WAR에서 `src/main/webapp/` 하위 정적 파일은 Servlet 컨테이너(Tomcat)가 직접 서빙한다. `UploadResourceConfig`의 `addResourceHandlers`는 `/profile/**` 전용이므로 `/css/**` 는 별도 등록 없이 Tomcat 기본 DefaultServlet이 처리한다. `src/main/resources/static/` 경로는 필요하지 않으며 신규 생성도 불필요하다. + +### 파일 경로 + +| 역할 | 경로 | +|---|---| +| 신규 — 전역 CSS | `src/main/webapp/css/global.css` | +| 수정 대상 — 인라인 토큰 제거 | `src/main/webapp/WEB-INF/views/*.jsp` (23개) | + +### global.css 선언 내용 (구체 명세) + +``` +/* 1. 라이트 테마 토큰 */ +html { + color-scheme: light; + --surface: #faf8f5; + --card-bg: #fff; + --text: #1a1a1a; + --text-muted: #5c5c5c; + --accent: #e8a54b; + --accent-soft: rgba(232, 165, 75, 0.16); + --border: rgba(0, 0, 0, 0.08); + --shadow: rgba(0, 0, 0, 0.06); + --field-bg: #fff; + --button-text: #1a1a1a; +} + +/* 2. 다크 테마 토큰 */ +html[data-theme="dark"] { + color-scheme: dark; + --surface: #121212; + --card-bg: #1e1e1e; + --text: #ece8e1; + --text-muted: #a39e96; + --border: rgba(255, 255, 255, 0.1); + --shadow: rgba(0, 0, 0, 0.35); + --field-bg: #181818; + --button-text: #1a1a1a; +} + +/* 3. body 기본 스타일 */ +body { + margin: 0; + min-height: 100vh; + font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans KR", sans-serif; + background: var(--surface); + color: var(--text); +} + +/* 4. admin-btn 컴포넌트 (admin 4개 파일 중복 제거) */ +.admin-btn { + min-height: 2.25rem; + padding: 0 0.85rem; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--card-bg); + color: var(--text); + font: inherit; + font-size: 0.8125rem; + font-weight: 800; + cursor: pointer; +} +.admin-btn:hover { border-color: rgba(232, 165, 75, 0.45); } +.admin-btn--primary { border-color: transparent; background: var(--accent); color: var(--button-text); } +.admin-btn--danger { border-color: rgba(200, 60, 60, 0.45); color: #c83c3c; } +``` + +**포함하지 않는 것**: 각 JSP 고유 레이아웃 클래스(`.admin-page`, `.form-page`, `.auth-main` 등), `max-width` 값(파일마다 48/72/78rem으로 다름), 페이지 전용 컴포넌트 CSS. 이들은 계속 인라인으로 유지한다. + +**`--danger` 토큰**: `admin-unity-feeds.jsp`에만 있는 `--danger: #c83c3c / #ef7878` 변수는 global.css에 추가하지 않는다. 해당 파일이 유일한 사용처이므로 인라인 유지가 맞다. + +**`--accent-soft` 미선언 파일 처리**: `login.jsp`와 `profile.jsp`는 `--accent-soft`를 선언하지 않는다. global.css에 전역 선언 후 이 두 파일의 인라인 블록에서 해당 변수가 없어도 문제 없다 (사용 위치가 없거나 다른 변수로 대체됨). 구현 시 각 파일의 실제 사용처를 확인하고 미사용이면 그냥 삭제, 사용이면 global.css 선언이 적용되므로 정상. + +### 마이그레이션 경로 (단계별) + +**Phase 1 — 파일 생성 + 1개 JSP 검증 (위험 최소)** +1. `src/main/webapp/css/global.css` 작성 +2. `login.jsp` 1개에만 `` 태그 추가 + 인라인 토큰 블록 제거 +3. 브라우저에서 라이트/다크 테마 전환 육안 검증 +4. 문제 없으면 Phase 2 진행 + +**Phase 2 — admin 4개 파일 적용 (`.admin-btn` 중복 제거 포함)** +1. `admin-post-categories.jsp`, `admin-unity-feeds.jsp`, `admin-console.jsp`, `admin-jam-list.jsp` 에 `` 추가 +2. 각 파일의 인라인 `html {}`, `html[data-theme="dark"] {}`, `body {}`, `.admin-btn*` 블록 제거 +3. admin 기능(등록/수정/삭제) 동작 검증 + +**Phase 3 — 나머지 19개 JSP 일괄 적용** +1. 남은 파일에 `` 추가 + 인라인 토큰 제거 +2. 각 페이지 라이트/다크 테마 시각 검증 + +### 위험 평가 + +| 위험 | 가능성 | 대응 | +|---|---|---| +| 일부 JSP가 인라인으로 토큰 값을 오버라이드하고 있다면 global.css 도입 후 이전과 동일 동작 | 낮음 (토큰 값 동일 확인됨) | Phase 1 검증 후 진행 | +| 특정 파일의 인라인 `html {}` 에 global.css에 없는 추가 변수 존재 (예: `--radius`, `--webgl-bg`) | 있음 (game-detail.jsp 확인) | 파일 전용 변수는 인라인 유지 — 중앙화 대상은 공통 8+4개 토큰만 | +| Tomcat DefaultServlet이 `/css/` 경로를 차단하는 보안 필터 존재 | 매우 낮음 | `/images/logo.png` 가 이미 동일 방식으로 서빙 중 — 검증됨 | +| FOUC: CSS 로드 전 테마 변수 미적용 깜빡임 | 낮음 | `theme-init.jsp`가 `` 최상단에서 `data-theme` 속성을 먼저 설정하고 있으므로 CSS 변수 로드 타이밍이 맞음 | + +--- + +## 권고 2 — JS 공용 유틸리티 분리 (P1) + +### 접근 + +`src/main/webapp/js/bibimbap-utils.js` 를 신규 생성하고 admin 4개 파일의 공통 함수(`post`, `handleResult`, `handleError`)를 이 파일로 추출한다. 이 파일은 `window.BibimbapUtils` 네임스페이스에 노출한다. + +`notify()`와 `openModal()` 은 title이 파일마다 달라 단순 통합이 불가능하므로 별도 전략을 쓴다(권고 3에서 다룸). + +각 JSP에서는 `` 직전에 `` 를 삽입하고, 기존 인라인 함수 선언을 제거한다. + +### 파일 경로 및 API 계약 (함수 시그니처) + +**신규 파일**: `src/main/webapp/js/bibimbap-utils.js` + +```javascript +window.BibimbapUtils = (function () { + + /** + * CSRF 토큰을 포함한 application/x-www-form-urlencoded POST 요청을 전송한다. + * + * @param {string} url - 요청 대상 URL + * @param {URLSearchParams|null} params - 요청 바디 파라미터 (null 이면 빈 URLSearchParams 사용) + * @param {string} csrfToken - X-CSRF-Token 헤더에 설정할 토큰 값 + * @returns {Promise} + */ + function post(url, params, csrfToken) { + // ... + } + + /** + * fetch Response가 ok이면 location.reload(), 아니면 오류 메시지를 콜백으로 전달한다. + * + * @param {Response} res - fetch가 반환한 Response 객체 + * @param {function(string)} onError - 오류 메시지 문자열을 받는 콜백 + */ + function handleResult(res, onError) { + // ... + } + + /** + * 네트워크/예외 오류 발생 시 오류 메시지를 콜백으로 전달한다. + * + * @param {function(string)} onError - 오류 메시지 문자열을 받는 콜백 + * @returns {function} catch 핸들러로 사용 가능한 함수 + */ + function makeErrorHandler(onError) { + // ... + } + + return { post: post, handleResult: handleResult, makeErrorHandler: makeErrorHandler }; +})(); +``` + +**시그니처 결정 이유**: + +- `post(url, params, csrfToken)`: 기존 admin 4개 파일의 `post()` 는 클로저로 `CSRF_TOKEN` 변수를 캡처하는 방식이었다. 전역 파일로 추출하면 클로저 캡처가 불가능하므로 `csrfToken`을 명시 파라미터로 받는다. 호출부가 항상 토큰을 직접 넘기므로 의도가 코드에 드러나고 타입 검사도 명확해진다. + +- `handleResult(res, onError)`: 기존 구현은 `notify()`를 직접 호출했다. `notify()`는 파일마다 title이 달라 전역화 불가능하므로, 대신 오류 메시지 문자열을 콜백으로 전달하는 방식으로 변경한다. 호출부가 메시지를 받아 직접 표시하므로 유연성과 분리가 동시에 달성된다. + +- `makeErrorHandler(onError)`: 기존 `handleError()`는 매직 문자열 `'요청 중 오류가 발생했습니다.'` 를 하드코딩했다. 유틸리티 레벨에서 특정 언어 문자열을 결정하지 않도록 콜백으로 위임한다. + +**inflate 경고**: `post()` 의 세 번째 파라미터 `csrfToken`이 항상 사용되는지 구현 시 확인 필요. 만약 호출부 중 하나라도 빈 문자열을 넘기는 패턴이 발견되면 `BibimbapCsrf.token()` 통합을 고려한다 (concerns 참조). + +### 마이그레이션 경로 + +1. `bibimbap-utils.js` 작성 (3개 함수 포함) +2. `admin-post-categories.jsp` 에 script 태그 추가 + 인라인 `post`, `handleResult`, `handleError` 함수 제거. `BibimbapUtils.post(...)`, `BibimbapUtils.handleResult(...)`, `BibimbapUtils.makeErrorHandler(...)` 로 호출부 수정 +3. 기능 검증 후 나머지 admin 3개 파일에 동일 적용 + +### 위험 평가 + +| 위험 | 가능성 | 대응 | +|---|---|---| +| admin 파일마다 `post()` 구현이 미묘하게 달라 통합 시 동작 변경 | 중간 | 통합 전 4개 파일 `post()` 구현 3-way diff 수행 (concerns 등록) | +| `handleResult` 시그니처 변경으로 기존 호출부 수정 필요 | 확실 | 마이그레이션 범위를 admin 4개로 한정, 변경이 넓지 않음 | + +--- + +## 권고 3 — 모달 사용 패턴 표준화 (P1) + +### 접근 + +**표준 패턴 채택**: `BibimbapModal` API (`window.BibimbapModal.alert / .confirm / .prompt`) 를 직접 호출하는 인라인 패턴을 표준으로 정한다. 기존 헬퍼 래퍼(`openModal()`, `notify()`)는 더 이상 신규 작성하지 않는다. + +**패턴 A (openModal 래퍼) 처리**: login/signup/game-register/profile 4개 파일의 `openModal()` 로컬 함수를 제거하고 호출부를 `window.BibimbapModal.alert({...})` 직접 호출로 대체한다. `BibimbapModal` 미존재 폴백은 `else { alert(message); }` 로 단순화한다. + +**패턴 B (notify 래퍼) 처리**: admin 4개 파일의 `notify()` 를 제거한다. 권고 2의 `BibimbapUtils.handleResult(res, onError)` 에서 `onError` 콜백이 `BibimbapModal.alert` 를 직접 호출하도록 호출부를 작성한다. 이렇게 하면 `notify()` 래퍼 없이도 동일 효과를 얻는다. + +**패턴 C (인라인 직접 체크) 처리**: posts-form/recruit-form/game-detail 은 이미 인라인이므로 현재 형태 유지. 다만 `game-detail.jsp` 내 `notifyError()` / `confirmAction()` 헬퍼(라인 1553-1561)는 파일 전용이므로 제거하지 않는다. 단일 파일에서의 로컬 헬퍼는 허용. + +**window.confirm 직접 사용 수정 (보안/일관성)**: +- `admin-post-categories.jsp:323` 의 `window.confirm()` → `BibimbapModal.confirm({...})` 으로 변경 +- `admin-unity-feeds.jsp:401` 의 `window.confirm()` → `BibimbapModal.confirm({...})` 으로 변경 + +이 두 곳은 삭제 확인 흐름이므로 `onConfirm` 콜백에 실제 삭제 로직을 이동한다. + +### 표준 패턴 예시 (참고용) + +```javascript +// 알림 (확인 버튼만) +if (window.BibimbapModal) { + window.BibimbapModal.alert({ title: '제목', message: '내용', confirmText: '확인' }); +} else { + alert('내용'); +} + +// 확인/취소 대화 +if (window.BibimbapModal) { + window.BibimbapModal.confirm({ + title: '삭제 확인', + message: '삭제하시겠습니까?', + onConfirm: function () { /* 삭제 로직 */ } + }); +} else { + if (window.confirm('삭제하시겠습니까?')) { /* 삭제 로직 */ } +} +``` + +### 파일 영향 맵 + +| 변경 유형 | 경로 | 역할 | +|---|---|---| +| 수정 — openModal 제거 | login.jsp, signup.jsp, game-register.jsp, profile.jsp | 패턴 A → 직접 호출 | +| 수정 — notify 제거 | admin-console.jsp, admin-jam-list.jsp, admin-post-categories.jsp, admin-unity-feeds.jsp | 패턴 B → 콜백 방식 | +| 수정 — window.confirm 교체 | admin-post-categories.jsp:323, admin-unity-feeds.jsp:401 | BibimbapModal.confirm 사용 | +| 유지 — 변경 없음 | game-detail.jsp (notifyError/confirmAction), posts-form.jsp, recruit-form.jsp | 파일 전용 헬퍼 허용 | + +### 마이그레이션 경로 + +1. `admin-post-categories.jsp` + `admin-unity-feeds.jsp` 의 `window.confirm` 2곳 먼저 수정 (보안 갭과 연결) +2. login/signup openModal 제거 + 직접 호출 전환 +3. game-register/profile 동일 처리 +4. admin 4개 파일 notify 제거 (권고 2와 동시 진행) + +### 위험 평가 + +| 위험 | 가능성 | 대응 | +|---|---|---| +| BibimbapModal이 modal.jsp 로드 전에 JS가 실행되는 경우 | 낮음 (header.jsp가 modal.jsp를 항상 include) | 변경 없이 현 구조 유지 | +| window.confirm 삭제 시 콜백 이동 누락으로 삭제 로직 미실행 | 중간 | 각 파일 수정 후 삭제 기능 수동 검증 | + +--- + +## 권고 4 — 폼 제출 유틸리티 + CSRF 갭 수정 (P0 — 보안) + +### 접근 + +**즉시 수정 (CSRF 갭)**: `recruit-form.jsp` 의 fetch 호출부에서 `BibimbapCsrf` 미존재 폴백(라인 390-394)이 CSRF 토큰을 전혀 포함하지 않는 문제를 수정한다. 두 가지 방법 중 **방법 A를 채택**한다. + +**방법 A (채택)**: 폴백 브랜치에 `hidden input`에서 추출한 토큰을 직접 삽입한다. + +```javascript +// recruit-form.jsp 수정안 +var csrfToken = (document.querySelector('input[name="_csrf"]') || {}).value || ''; +fetch(form.action, { + method: 'POST', + headers: window.BibimbapCsrf ? window.BibimbapCsrf.headers({ + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', + 'Accept': 'application/json', + 'X-Requested-With': 'XMLHttpRequest' + }) : { + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', + 'Accept': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + 'X-CSRF-Token': csrfToken // 갭 수정 + }, + body: body +}) +``` + +**전제**: `recruit-form.jsp` 에 `` hidden input을 추가해야 한다. 현재 `recruit-form.jsp` 에는 이 hidden input이 없다. 추가하면 `new FormData(form)` 이 자동으로 `_csrf` 파라미터를 포함하므로 서버사이드 form 파라미터 검증도 함께 강화된다. + +**방법 B (미채택)**: `theme-init.jsp` 의 `BibimbapCsrf` 를 항상 신뢰하여 폴백 분기 자체를 제거. 단, `BibimbapCsrf` 미존재 케이스를 완전히 제거하면 `theme-init.jsp` 가 로드 실패 시 CSRF 토큰이 아예 없어지는 더 큰 갭이 생긴다. 따라서 채택하지 않는다. + +**401 리다이렉트 불일치 수정**: `recruit-form.jsp` 의 401 처리를 `posts-form.jsp` 와 동일하게 `redirectLogin` 함수로 분리한다. + +**성공 시 폴백 불일치 수정**: `recruit-form.jsp:419-421` 의 `alert(...)` 호출을 `posts-form.jsp:260-261` 패턴에 맞게 `alert` 없이 `go()` 만 호출하도록 변경한다. + +### 파일 영향 맵 + +| 변경 유형 | 경로 | 역할 | +|---|---|---| +| 수정 (보안) | recruit-form.jsp | hidden _csrf input 추가 + 폴백 브랜치 토큰 삽입 | +| 수정 (일관성) | recruit-form.jsp | 401 처리 redirectLogin 함수 분리 | +| 수정 (일관성) | recruit-form.jsp | 성공 폴백 alert 제거 | +| 변경 없음 | posts-form.jsp | 현행 유지 (기준 파일) | + +### 마이그레이션 경로 + +1. `recruit-form.jsp` 에 `` 추가 (JSP EL 또는 request attribute 사용) +2. JS 폴백 브랜치에 `X-CSRF-Token` 헤더 추가 +3. 401 처리 함수 분리 +4. 성공 폴백 통일 +5. 실제 폼 제출(등록) + 401 시나리오(로그아웃 후 제출) 수동 검증 + +### 위험 평가 + +| 위험 | 가능성 | 대응 | +|---|---|---| +| hidden _csrf input 추가 시 서버 컨트롤러가 기대하는 파라미터명 불일치 | 낮음 | posts-form.jsp와 동일 파라미터명 `_csrf` 사용 | +| BibimbapCsrf가 항상 존재한다고 가정하고 폴백을 제거하고 싶은 유혹 | 중간 | 방법 B 미채택 이유 참조 — 폴백 브랜치 유지 | + +--- + +## 권고 5 — 날짜 포맷 유틸리티 모듈 (P2) + +### 접근 + +`game-detail.jsp` 인라인의 `fmtAbsolute`, `fmtRelative`, `buildTimeEl` 세 함수를 `src/main/webapp/js/bibimbap-date.js` 로 추출하고 `window.BibimbapDate` 네임스페이스에 노출한다. + +`game-detail.jsp` 는 `` 를 추가하고 기존 인라인 선언을 제거한다. 다른 detail 페이지(posts-detail, recruit-detail, jam-detail)에서 날짜 포맷이 필요해질 때 이 파일을 include하면 된다 — 현재는 필요하지 않으므로 강제 적용하지 않는다. + +### 파일 경로 및 API 계약 (함수 시그니처) + +**신규 파일**: `src/main/webapp/js/bibimbap-date.js` + +```javascript +window.BibimbapDate = (function () { + + /** + * ISO 8601 문자열을 ko-KR 로케일 절대 날짜/시각 문자열로 변환한다. + * + * @param {string} iso - ISO 8601 날짜 문자열 + * @returns {string} - "2026. 6. 30. 오전 10:00:00" 형식, 파싱 실패 시 빈 문자열 + */ + function fmtAbsolute(iso) { ... } + + /** + * ISO 8601 문자열을 상대 시각 문자열로 변환한다 (7일 이내: "n분/시간/일 전", 초과: 절대). + * + * @param {string} iso - ISO 8601 날짜 문자열 + * @returns {string} - "3시간 전" 또는 절대 날짜, 파싱 실패 시 빈 문자열 + */ + function fmtRelative(iso) { ... } + + /** + *