fix(redesign): 골드 토큰 통일·클래스 충돌 정리

리디자인 검토에서 발견된 디자인 일관성 결함 수정.

- bibimbap.css: --color-accent #D4A853→#e8a54b(light+dark),
  --color-accent-press #C79A40→#d8973a. 앱 전역 브랜드 골드
  (#e8a54b, header/footer/modal/26파일)와 어긋나 모든 페이지에
  골드 2종이 공존하던 문제 해소. + .btn/.chip/.link/a focus-visible 링.
- profile.jsp: 페이지-로컬 .profile-* 를 source of truth 채택,
  합집합으로 붙은 중복 bibimbap 유틸(btn/avatar/thumb/meta/game-row
  /profile-head/card/section-*) 제거 → override 모호성 소멸.
- recruit-form.jsp: bibimbap 와 충돌하던 로컬 클래스(field/form-grid
  /req/count/preview-card/form-actions) rf- prefix rename. ID·JS 무변경.
- index.jsp: 중복 <link bibimbap.css> 제거(theme-init 가 전역 로드).

L1 mvnw package exit 0. L2 dev bind-mount 라이브 스모크 /·/login
/signup/terms/recruit/posts/error HTTP 200 + JSP 에러 0. 인증 게이트
페이지(recruit-form/profile) 렌더는 needs_user_verification.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0162BaZnrbiYgWc9JJMZ9Czb
This commit is contained in:
이정수 2026-06-30 17:46:32 +09:00
parent d379a1c353
commit cf1e7cea62
6 changed files with 120 additions and 72 deletions

View File

@ -0,0 +1,38 @@
# 2026-06-30 리디자인 후속 — 토큰 통일·클래스 충돌 정리
**세션**: sid 20260630-170024
**브랜치**: feat/v2
**선행**: [2026-06-30-visual-redesign-bibimbap-css.md](./2026-06-30-visual-redesign-bibimbap-css.md) 검토에서 발견된 일관성 결함 수정
## 배경
리디자인 비주얼 레이어 검토 결과 디자인 일관성 결함 발견 — 공유 `bibimbap.css`
앱 전역 브랜드 골드 `#e8a54b`(header/footer/modal/26파일) 와 다른 `#D4A853`
도입해 모든 페이지에 골드 2종이 공존했다. 부수적으로 클래스 합집합 override 모호성과
shadowing, 이중 CSS 로드가 있었다.
## 변경
| 항목 | 파일 | 내용 |
|---|---|---|
| accent 통일 | `css/bibimbap.css` | `--color-accent` `#D4A853→#e8a54b`(light+dark), `--color-accent-press` `#C79A40→#d8973a`. 앱 전역 브랜드 골드로 단일화. |
| focus-visible | `css/bibimbap.css` | `.btn`/`.chip`/`.link`/`a` 키보드 포커스 링 추가(마우스 클릭 시 미표시). |
| 클래스 충돌 제거 | `views/profile.jsp` | 페이지-로컬 `.profile-*` 를 source of truth 채택, 합집합으로 붙은 중복 bibimbap 유틸(`btn`/`avatar`/`thumb`/`meta`/`game-row`/`profile-head`/`card`/`section-*`) 제거 → override 모호성 소멸. |
| shadowing 해소 | `views/recruit-form.jsp` | bibimbap 와 충돌하던 로컬 클래스명(`field`/`form-grid`/`req`/`count`/`preview-card`/`form-actions`) `rf-` prefix rename. ID·JS·accent 무변경(이미 브랜드 골드 사용). |
| 이중 로드 제거 | `views/index.jsp` | 직접 `<link bibimbap.css>` 제거(theme-init 가 이미 전역 로드). |
인라인 핸들러(login/signup `oninput`·`onclick`)는 기능 정상·외부화 시 회귀위험 → **유지**(수용).
## 검증
| 레벨 | 결과 |
|---|---|
| L1 — WAR 빌드 | `./mvnw -q -DskipTests package` exit 0 PASS. WAR 내 `css/bibimbap.css` 포함 확인. |
| L2 — 런타임 스모크 | dev 컨테이너 소스 bind-mount(`.:/build`)로 변경 즉시 라이브. `/` `/login` `/signup` `/terms` `/recruit` `/posts` `/error` HTTP 200 + JSP 컴파일 에러 0. 서빙 css accent `#e8a54b` 확인. |
| L2 — 인증 게이트 | `/recruit/new`·`/profile` HTTP 302(로그인 필요) → JSP 렌더 미확인. |
## needs_user_verification
- `/recruit/new`(모집 폼): 로그인 세션에서 rf- rename 후 레이아웃·라이브 미리보기 정상 렌더.
- `/profile`: 로그인 세션에서 버튼/카드 외형(중복 클래스 제거 후) + 골드 정합.
- 다크모드 토글 시 전 페이지 골드 단일(`#e8a54b`) 육안 정합.

View File

@ -11,4 +11,5 @@
- [2026-06-24-w2-jam-platform.md](./2026-06-24-w2-jam-platform.md) — W2 게임잼 워크스트림 전체(W2-1~6) 구현. 잼 엔티티/라이프사이클(jams/jam_teams/jam_team_members/jam_entries/jam_status_log) + 심사위원 역할(jam_judges 잼스코프) + 평가 동결 스키마(jam_criteria/jam_scores/jam_votes/jam_awards + jam_score_stats VIEW, 평가단위 (jam_id,game_id) 활성 자연키) + 심사 평가(3중게이트 UPSERT 가중집계) + 인기투표(1인1표 UNIQUE·종료후 공개) + 시상 집계(3트랙+가중 GRAND·CLOSED 확정 멱등). GAME_JAM_MANAGE 첫 enforcement 연결·잼스코프 isJudge·평가기간 게이트·CSRF 전수. 6 커밋 ccf1e42~a74bf74. 최종 L1 190/190 GREEN(회귀 0), L2 dev DB contract 전 PASS(격리 throwaway DB). dev DB 마이그레이션·L3 스모크 needs_user_verification.
- [2026-06-29-b2-b4-fe-hardening.md](./2026-06-29-b2-b4-fe-hardening.md) — B2 프로토타입 dead code 제거(abstracts 4파일·header.jspf·GameCatalog 삭제, 없는 게임 ID = HTTP 404) + B4 의존성/세션 하드닝(Spring Boot 3.5.16 GA·snapshot repo 제거·쿠키 HttpOnly/SameSite·로그 WARN·OWASP DC plugin 비bind·profile properties 신규) + FE 다크모드 대비·검색 접이식·터치타깃. mvn test 361 PASS.
- [2026-06-30-screenshot-guide-auth.md](./2026-06-30-screenshot-guide-auth.md) — 스크린샷 가이드 인증 흐름 추가. 비인증 7장 → 인증 포함 최대 14장. Playwright AJAX 로그인(expect_navigation) + SCREENSHOT_EMAIL/PASSWORD env var 분기 + 리뷰/댓글 composer hidden 강제 노출 폴백.
- [2026-06-30-redesign-token-coherence-fix.md](./2026-06-30-redesign-token-coherence-fix.md) — 리디자인 후속 일관성 수정. 공유 bibimbap.css accent 를 앱 전역 브랜드 골드로 통일(`#D4A853→#e8a54b`, light+dark) + focus-visible 링 + profile.jsp 중복 유틸 클래스 제거(override 모호성 소멸) + recruit-form.jsp 충돌 클래스 `rf-` rename(shadowing 해소) + index.jsp 이중 CSS 로드 제거. L1 빌드 PASS, L2 스모크 7페이지 200(JSP 에러 0). 인증 게이트 페이지(recruit-form/profile) 렌더는 needs_user_verification.
- [2026-06-30-visual-redesign-bibimbap-css.md](./2026-06-30-visual-redesign-bibimbap-css.md) — 클로드 디자인 리디자인 비주얼 레이어 9단위 반영. bibimbap.css 신설 + theme-init 공통 link + 빈상태 fragment 2종 + 홈/인증/약관/프로필/에러/모집 마크업·클래스 이식. game-detail no-op. 커밋 8개, L2 스모크 전 페이지 200(errer JSTL taglib 결함 포착·수정). 인터랙티브 제출·다크모드는 needs_user_verification.

View File

@ -116,7 +116,6 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<jsp:include page="/WEB-INF/views/theme-init.jsp"/>
<link rel="stylesheet" href="${pageContext.request.contextPath}/css/bibimbap.css">
<title>bibimbap</title>
<style>
html {

View File

@ -434,13 +434,13 @@
<main class="profile-main">
<section class="profile-shell" aria-labelledby="profile-title">
<div class="profile-heading">
<p class="profile-heading__eyebrow section-eyebrow">BIBIMBAP ACCOUNT</p>
<h1 class="profile-heading__title section-title" id="profile-title">프로필</h1>
<p class="profile-heading__eyebrow">BIBIMBAP ACCOUNT</p>
<h1 class="profile-heading__title" id="profile-title">프로필</h1>
</div>
<div class="profile-panel">
<div class="profile-summary profile-head">
<div class="profile-avatar avatar" aria-hidden="true">
<div class="profile-summary">
<div class="profile-avatar" aria-hidden="true">
<img id="profile-avatar-img" src="<%= avatarUrl %>" alt="" width="96" height="96" <%= avatarUrl.isBlank() ? "hidden" : "" %> />
<span id="profile-avatar-initial" <%= avatarUrl.isBlank() ? "" : "hidden" %>><%= avatarInitial %></span>
</div>
@ -463,14 +463,14 @@
</div>
<div class="profile-actions">
<a class="profile-button profile-button--primary btn btn-primary" href="<%= ctx %>/">홈으로 이동</a>
<a class="profile-button profile-button--primary" href="<%= ctx %>/">홈으로 이동</a>
<form action="<%= ctx %>/logout" method="post">
<button class="profile-button btn btn-ghost" type="submit">로그아웃</button>
<button class="profile-button" type="submit">로그아웃</button>
</form>
</div>
</div>
<div class="profile-games card">
<div class="profile-games">
<div class="profile-games__head">
<h2 class="profile-games__title">내 게임</h2>
<span class="profile-games__count"><%= myGames.size() %>개</span>
@ -494,15 +494,15 @@
}
boolean visible = game.getVisible() == null || game.getVisible();
%>
<div class="profile-game game-row">
<a class="profile-game__thumb thumb" href="<%= ctx %>/game/<%= game.getId() %>" aria-hidden="true" tabindex="-1">
<div class="profile-game">
<a class="profile-game__thumb" href="<%= ctx %>/game/<%= game.getId() %>" aria-hidden="true" tabindex="-1">
<% if (hasImage) { %>
<img src="<%= thumbUrl %>" alt="" loading="lazy" decoding="async" />
<% } else { %>
<img class="profile-game__fallback" src="<%= ctx %>/images/logo.png" alt="" width="40" height="40" />
<% } %>
</a>
<div class="profile-game__body meta">
<div class="profile-game__body">
<a class="profile-game__name" href="<%= ctx %>/game/<%= game.getId() %>"><%= gameName %></a>
<div class="profile-game__meta">
<span class="profile-game__status <%= visible ? "profile-game__status--visible" : "" %>"><%= visible ? "공개" : "비공개" %></span>
@ -510,7 +510,7 @@
</div>
</div>
<div class="profile-game__actions">
<a class="profile-game__action btn btn-ghost" href="<%= ctx %>/game/<%= game.getId() %>/edit">수정</a>
<a class="profile-game__action" href="<%= ctx %>/game/<%= game.getId() %>/edit">수정</a>
</div>
</div>
<% } %>

View File

@ -66,7 +66,7 @@
align-items: start;
}
.recruit-form,
.preview-card {
.rf-preview {
border: 1px solid var(--border);
border-radius: 12px;
background: var(--card-bg);
@ -75,26 +75,26 @@
.recruit-form {
padding: 1.35rem;
}
.form-grid {
.rf-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.9rem;
}
.field {
.rf-field {
display: grid;
gap: 0.4rem;
}
.field--full {
.rf-field--full {
grid-column: 1 / -1;
}
.field label {
.rf-field label {
color: var(--text);
font-size: 0.8125rem;
font-weight: 900;
}
.field input,
.field select,
.field textarea {
.rf-field input,
.rf-field select,
.rf-field textarea {
width: 100%;
box-sizing: border-box;
border: 1px solid var(--border);
@ -104,7 +104,7 @@
font: inherit;
font-size: 0.9375rem;
}
.field select {
.rf-field select {
appearance: none;
cursor: pointer;
padding-right: 3rem;
@ -115,38 +115,38 @@
background-position: 0 0, right 0.82rem center;
background-size: 100% 100%, 1.05rem 1.05rem;
}
.field select::-ms-expand {
.rf-field select::-ms-expand {
display: none;
}
.field select:hover {
.rf-field select:hover {
border-color: rgba(232, 165, 75, 0.45);
}
.field select option {
.rf-field select option {
background: var(--field-bg);
color: var(--text);
}
.field input,
.field select {
.rf-field input,
.rf-field select {
height: 2.85rem;
padding: 0 0.8rem;
}
.field select {
.rf-field select {
padding-right: 3rem;
}
.field textarea {
.rf-field textarea {
min-height: 8rem;
padding: 0.8rem;
line-height: 1.6;
resize: vertical;
}
.field input:focus,
.field select:focus,
.field textarea:focus {
.rf-field input:focus,
.rf-field select:focus,
.rf-field textarea:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(232, 165, 75, 0.22);
}
.form-actions {
.rf-actions {
margin-top: 1rem;
display: flex;
justify-content: flex-end;
@ -172,34 +172,34 @@
background: var(--accent);
color: #1a1a1a;
}
.req {
.rf-req {
color: var(--accent);
font-weight: 900;
margin-left: 0.15em;
}
.field-head {
.rf-field-head {
display: flex;
justify-content: space-between;
align-items: baseline;
}
.count {
.rf-count {
font-size: 0.75rem;
color: var(--text-muted);
font-weight: 400;
white-space: nowrap;
}
.preview-card {
.rf-preview {
position: sticky;
top: 5rem;
padding: 1.1rem;
}
.preview-card__label {
.rf-preview__label {
margin: 0 0 0.65rem;
color: var(--text-muted);
font-size: 0.75rem;
font-weight: 900;
}
.preview-card__role {
.rf-preview__role {
display: inline-flex;
align-items: center;
min-height: 1.85rem;
@ -211,21 +211,21 @@
font-size: 0.75rem;
font-weight: 900;
}
.preview-card h2 {
.rf-preview h2 {
margin: 0;
font-size: 1.2rem;
line-height: 1.35;
letter-spacing: 0;
word-break: break-word;
}
.preview-card__line {
.rf-preview__line {
margin: 0.55rem 0 0;
color: var(--text-muted);
font-size: 0.875rem;
line-height: 1.6;
word-break: break-word;
}
.preview-card__meta {
.rf-preview__meta {
margin: 1rem 0 0;
padding: 0;
list-style: none;
@ -234,22 +234,22 @@
color: var(--text-muted);
font-size: 0.8125rem;
}
.preview-card__meta strong {
.rf-preview__meta strong {
color: var(--text);
}
@media (max-width: 860px) {
.form-layout {
grid-template-columns: 1fr;
}
.preview-card {
.rf-preview {
position: static;
}
}
@media (max-width: 560px) {
.form-grid {
.rf-grid {
grid-template-columns: 1fr;
}
.form-actions {
.rf-actions {
flex-direction: column-reverse;
}
.form-button {
@ -270,31 +270,31 @@
<div class="form-layout">
<section class="recruit-form" aria-label="팀원 모집글 입력">
<form id="recruit-form" action="<%= ctx %>/recruit/new" method="post" novalidate>
<div class="form-grid">
<div class="field">
<label for="project-name">프로젝트명 <span class="req">*</span></label>
<div class="rf-grid">
<div class="rf-field">
<label for="project-name">프로젝트명 <span class="rf-req">*</span></label>
<input id="project-name" name="projectName" type="text" maxlength="80" placeholder="예: 달빛 정거장" required>
</div>
<div class="field">
<div class="rf-field">
<label for="genre">게임 장르</label>
<input id="genre" name="genre" type="text" maxlength="60" placeholder="예: 2D 어드벤처">
</div>
<div class="field field--full">
<div class="field-head">
<label for="summary">한 줄 소개 <span class="req">*</span></label><span class="count"><span id="sc">0</span> / 120</span>
<div class="rf-field rf-field--full">
<div class="rf-field-head">
<label for="summary">한 줄 소개 <span class="rf-req">*</span></label><span class="rf-count"><span id="sc">0</span> / 120</span>
</div>
<input id="summary" name="summary" type="text" maxlength="120" placeholder="프로젝트를 한 문장으로 소개해 주세요." required
oninput="document.getElementById('sc').textContent=this.value.length">
</div>
<div class="field">
<label for="role">모집 역할 <span class="req">*</span></label>
<div class="rf-field">
<label for="role">모집 역할 <span class="rf-req">*</span></label>
<select id="role" name="role">
<option selected>기획</option>
<option>아트</option>
<option>프로그래머</option>
</select>
</div>
<div class="field">
<div class="rf-field">
<label for="status">진행 상태</label>
<select id="status" name="status">
<option selected>아이디어 단계</option>
@ -303,7 +303,7 @@
<option>출시 준비 중</option>
</select>
</div>
<div class="field">
<div class="rf-field">
<label for="type">참여 방식</label>
<select id="type" name="type">
<option selected>취미</option>
@ -312,36 +312,36 @@
<option>게임잼</option>
</select>
</div>
<div class="field">
<div class="rf-field">
<label for="period">예상 기간</label>
<input id="period" name="period" type="text" maxlength="40" placeholder="예: 약 3개월">
</div>
<div class="field">
<div class="rf-field">
<label for="team">현재 팀 구성</label>
<input id="team" name="team" type="text" maxlength="80" placeholder="예: 기획 1명, 프로그래머 1명">
</div>
<div class="field">
<label for="contact">연락 방법 <span class="req">*</span></label>
<div class="rf-field">
<label for="contact">연락 방법 <span class="rf-req">*</span></label>
<input id="contact" name="contact" type="text" maxlength="100" placeholder="이메일, 디스코드, 오픈채팅 등" required>
</div>
<div class="field field--full">
<div class="rf-field rf-field--full">
<label for="description">상세 설명</label>
<textarea id="description" name="description" maxlength="1200" placeholder="프로젝트 소개, 원하는 역할, 작업 방식 등을 적어 주세요."></textarea>
</div>
</div>
<div class="form-actions">
<div class="rf-actions">
<a class="form-button" href="<%= ctx %>/recruit">취소</a>
<button class="form-button form-button--primary" type="submit">모집글 등록</button>
</div>
</form>
</section>
<aside class="preview-card" aria-label="모집글 미리보기">
<p class="preview-card__label">미리보기</p>
<span class="preview-card__role" id="preview-role">기획 모집</span>
<aside class="rf-preview" aria-label="모집글 미리보기">
<p class="rf-preview__label">미리보기</p>
<span class="rf-preview__role" id="preview-role">기획 모집</span>
<h2 id="preview-name">프로젝트 이름</h2>
<p class="preview-card__line" id="preview-summary">한 줄 소개가 여기에 표시됩니다.</p>
<ul class="preview-card__meta">
<p class="rf-preview__line" id="preview-summary">한 줄 소개가 여기에 표시됩니다.</p>
<ul class="rf-preview__meta">
<li><strong>장르</strong> <span id="preview-genre">장르 미정</span></li>
<li><strong>상태</strong> <span id="preview-status">아이디어 단계</span></li>
<li><strong>참여</strong> <span id="preview-type">취미</span></li>
@ -349,7 +349,7 @@
<li><strong>팀</strong> <span id="preview-team">팀 구성 미정</span></li>
<li><strong>연락</strong> <span id="preview-contact">연락 방법 미정</span></li>
</ul>
<p class="preview-card__line" id="preview-description">상세 설명이 여기에 표시됩니다.</p>
<p class="rf-preview__line" id="preview-description">상세 설명이 여기에 표시됩니다.</p>
</aside>
</div>
</main>

View File

@ -10,10 +10,10 @@
--color-bg: #F5F0E8; /* 페이지 배경 */
--color-card: #FFFFFF; /* 카드 표면 */
--color-field-bg: #FBF8F2; /* 입력란 배경 */
--color-accent: #D4A853; /* 골든 액센트(주요 버튼) */
--color-accent: #e8a54b; /* 골든 액센트(주요 버튼) — 브랜드 골드 통일 */
--color-accent-strong: #C2912F; /* 링크·강조 텍스트 */
--color-accent-soft: #FBEFD6; /* 액센트 옅은 배경(뱃지) */
--color-accent-press: #C79A40; /* 버튼 hover/active */
--color-accent-press: #d8973a; /* 버튼 hover/active */
--color-text: #241F18; /* 본문 텍스트 */
--color-text-muted: #6E6555; /* 보조 텍스트 */
@ -61,10 +61,10 @@ html[data-theme="dark"] {
--color-border: rgba(255, 255, 255, 0.08); /* header --header-border 소폭 강조 */
--color-border-soft: rgba(255, 255, 255, 0.06); /* header --header-border */
--color-accent: #D4A853; /* 라이트와 동일 */
--color-accent: #e8a54b; /* 브랜드 골드 — 라이트와 동일 */
--color-accent-strong: #e8a54b; /* header --accent (다크에서 밝게) */
--color-accent-soft: rgba(212, 168, 83, 0.15); /* 옅은 골든 배경 */
--color-accent-press: #C79A40; /* 라이트와 동일 */
--color-accent-press: #d8973a; /* 라이트와 동일 */
--color-danger: #e07050; /* 다크에서 가독 보강 */
--color-danger-bg: rgba(178, 74, 44, 0.18);
@ -83,6 +83,16 @@ body {
*, *::before, *::after { box-sizing: border-box; }
::selection { background: #F1D79B; }
/* 키보드 포커스 링 (마우스 클릭 시엔 미표시) */
.btn:focus-visible,
.chip:focus-visible,
.link:focus-visible,
a:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
border-radius: var(--radius-sm);
}
/* main 영역 공통 컨테이너(선택) */
.page { max-width: 1120px; margin: 0 auto; padding: 48px 24px; }
.page--center { display: flex; justify-content: center; }