Skip to content
53 changes: 35 additions & 18 deletions .specify/scripts/bash/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,24 +28,29 @@ get_current_branch() {

# For non-git repos, try to find the latest feature directory
local repo_root=$(get_repo_root)
local specs_dir="$repo_root/specs/feat"
local specs_base="$repo_root/specs"

if [[ -d "$specs_dir" ]]; then
if [[ -d "$specs_base" ]]; then
local latest_feature=""
local highest=0

for dir in "$specs_dir"/*; do
if [[ -d "$dir" ]]; then
local dirname=$(basename "$dir")
if [[ "$dirname" =~ ^([0-9]+)- ]]; then
local number=${BASH_REMATCH[1]}
number=$((10#$number))
if [[ "$number" -gt "$highest" ]]; then
highest=$number
latest_feature=$dirname
for type_dir in "$specs_base"/*/; do
[[ -d "$type_dir" ]] || continue
local type_name
type_name="$(basename "$type_dir")"
for dir in "$type_dir"*/; do
if [[ -d "$dir" ]]; then
local dirname=$(basename "$dir")
if [[ "$dirname" =~ ^([0-9]+)- ]]; then
local number=${BASH_REMATCH[1]}
number=$((10#$number))
if [[ "$number" -gt "$highest" ]]; then
highest=$number
latest_feature="${type_name}/#${dirname}"
fi
fi
fi
fi
done
done

if [[ -n "$latest_feature" ]]; then
Expand Down Expand Up @@ -107,29 +112,41 @@ check_feature_branch() {
return 1
}

get_feature_dir() { echo "$1/specs/feat/$2"; }
get_feature_dir() {
local repo_root="$1"
local branch_name="$2"
find_feature_dir_by_prefix "$repo_root" "$branch_name"
}

# Find feature directory by issue number or numeric prefix
# Supports: feat/#96-social-login → specs/feat/096-*
# 096-social-login → specs/feat/096-*
# Supports: fix/#441-slug → specs/fix/441-*
# feat/#96-social-login → specs/feat/096-*
# 096-social-login → specs/feat/096-* (legacy)
find_feature_dir_by_prefix() {
local repo_root="$1"
local branch_name="$2"
local specs_dir="$repo_root/specs/feat"

# Extract type prefix from branch name (e.g., fix/#441-slug → fix, feat/#96-slug → feat)
local type_prefix="feat" # default for legacy branches
if [[ "$branch_name" =~ ^([a-z]+)/#[0-9]+ ]]; then
type_prefix="${BASH_REMATCH[1]}"
fi
Comment on lines +130 to +133
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The type_prefix defaults to "feat" for legacy branch names (those not following the type/#number pattern). This means if a legacy branch (e.g., 441-mute-timer-layout-fix) refers to a specification that has been moved to specs/fix/, this script will fail to locate it. To improve robustness, consider iterating through all subdirectories of specs/ to find a matching issue number if the branch name doesn't explicitly include a type prefix, similar to the logic implemented in get_current_branch.


local specs_dir="$repo_root/specs/$type_prefix"

# Extract issue number from branch name
local issue_num=$(extract_issue_number "$branch_name")

if [[ -z "$issue_num" ]]; then
# If no issue number found, fall back to exact match under specs/feat/
# If no issue number found, fall back to exact match under specs/{type}/
echo "$specs_dir/$branch_name"
return
fi

# Zero-pad to 3 digits for matching
local padded=$(printf "%03d" "$((10#$issue_num))")

# Search for directories in specs/feat/ that start with this prefix
# Search for directories in specs/{type}/ that start with this prefix
local matches=()
if [[ -d "$specs_dir" ]]; then
for dir in "$specs_dir"/"$padded"-*; do
Expand Down
37 changes: 37 additions & 0 deletions specs/fix/441-mute-timer-layout-fix/checklists/requirements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Specification Quality Checklist: 음소거 아이콘 헤더 반영 및 타이머 화면 레이아웃 개선

**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-04-04
**Feature**: [spec.md](../spec.md)

## Content Quality

- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed

## Requirement Completeness

- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified

## Feature Readiness

- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification

## Notes

- 음소거 아이콘(FR-001~002)과 레이아웃(FR-003~009)은 독립적으로 구현/테스트 가능
- 볼륨 0 = 음소거 동작 정책은 명확화를 통해 확정됨 (세션 2026-04-04)
- 두 줄 레이아웃 정렬 방식(중앙)은 명확화를 통해 확정됨 (세션 2026-04-04)
- 타이머 설정 화면은 이번 수정 범위에서 제외됨
49 changes: 49 additions & 0 deletions specs/fix/441-mute-timer-layout-fix/data-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Data Model: 음소거 아이콘 헤더 반영 및 타이머 화면 레이아웃 개선

> 이 픽스는 순수 UI 레이어 수정이므로 새로운 타입/엔티티 추가 없음.
> 기존 타입을 그대로 활용.

## 관련 기존 타입

### `TimerPageLogics` (src/page/TimerPage/hooks/useTimerPageState.ts)

```ts
export interface TimerPageLogics {
// ... 기존 필드 ...
volume: number; // 0~10 정수. 0이면 음소거 상태
setVolume: (value: number) => void;
isVolumeBarOpen: boolean;
toggleVolumeBar: () => void;
volumeRef: React.RefObject<HTMLDivElement>;
}
```

**`isMuted` 파생 로직**: `const isMuted = volume === 0;`
- `TimerPage.tsx` 내에서 파생하여 사용
- `TimerPageLogics` 인터페이스 변경 없음

### `NormalTimerProps` (src/page/TimerPage/components/NormalTimer.tsx)

```ts
interface NormalTimerProps {
normalTimerInstance: NormalTimerInstance;
isAdditionalTimerAvailable: boolean;
item: TimeBoxInfo; // item.speaker: string | null
teamName: string | null;
}
```

**레이아웃 로직 변경**:
- `teamName` → 첫 번째 줄 (truncate 적용)
- `item.speaker` → 두 번째 줄 (완전 표시)
- 두 줄 모두 중앙 정렬

## 변경 영향 범위

| 파일 | 변경 종류 | 설명 |
|---|---|---|
| `src/page/TimerPage/TimerPage.tsx` | 수정 | volume === 0 분기로 음소거 아이콘 조건부 렌더링 |
| `src/page/TimerPage/components/NormalTimer.tsx` | 수정 | 두 줄 레이아웃 + h1 text-center 추가 |
| `src/components/icons/` | 변경 없음 | DTVolumeMuted 미생성, react-icons 사용 |
| `src/page/TimerPage/hooks/useTimerPageState.ts` | 변경 없음 | 인터페이스 그대로 유지 |
| `src/apis/` | 변경 없음 | API 호출 없음 |
119 changes: 119 additions & 0 deletions specs/fix/441-mute-timer-layout-fix/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# Implementation Plan: 음소거 아이콘 헤더 반영 및 타이머 화면 레이아웃 개선

**Branch**: `fix/#441-mute-timer-layout-fix` | **Date**: 2026-04-04 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `/specs/fix/441-mute-timer-layout-fix/spec.md`

## Summary

헤더의 볼륨 버튼 아이콘이 음소거 상태를 반영하지 않는 버그와
`NormalTimer`에서 팀명과 토론자 정보가 한 줄로 압축되는 레이아웃 버그를 수정한다.
순수 UI 레이어 변경(2개 파일 수정)으로, API/상태관리 변경 없음.

## Technical Context

**Language/Version**: TypeScript 5.7 (strict mode)
**Primary Dependencies**: React 18, Tailwind CSS 3, react-icons 5, react-i18next
**Storage**: N/A (localStorage 볼륨 값 - 기존 로직 유지)
**Testing**: Vitest + @testing-library/react + @testing-library/user-event + MSW
**Target Platform**: Web (Chrome/Safari/Firefox)
**Project Type**: Web SPA (Vite + React Router v7)
**Performance Goals**: 즉각적인 아이콘 상태 변경 (React 상태 업데이트 기반, 별도 성능 목표 없음)
**Constraints**: 기존 TimerPageLogics 인터페이스 변경 없음, API 호출 없음
**Scale/Scope**: TimerPage 1개 + NormalTimer 1개 컴포넌트 수정

## Constitution Check

✅ **레이어드 폴더 구조**: 수정 대상 파일이 기존 구조(`page/TimerPage/`, `page/TimerPage/components/`) 내 위치
✅ **코드 스타일**: function declaration 유지, camelCase/PascalCase 준수
✅ **TDD**: 테스트 파일 먼저 작성 후 구현 예정
✅ **i18n**: 기존 `t()` 사용 패턴 유지, 하드코딩 텍스트 없음
✅ **API 레이어 패턴**: API 변경 없음
✅ **순환 의존성**: 없음

**Constitution 위반 사항**: 없음

## Project Structure

### Documentation (this feature)

```text
specs/fix/441-mute-timer-layout-fix/
├── plan.md # 이 파일
├── research.md # Phase 0 output ✅
├── data-model.md # Phase 1 output ✅
├── test-contracts/
│ ├── NormalTimer.md # Phase 1 output ✅
│ └── TimerPage-mute-icon.md # Phase 1 output ✅
└── tasks.md # Phase 2 output (/speckits:tasks 로 생성)
```

### Source Code (수정 대상)

```text
src/
├── page/TimerPage/
│ ├── TimerPage.tsx # 수정: 음소거 시 다른 아이콘 표시
│ ├── TimerPage.test.tsx # 신규: 헤더 음소거 아이콘 TDD 테스트
│ └── components/
│ ├── NormalTimer.tsx # 수정: 두 줄 레이아웃 + text-center
│ └── NormalTimer.test.tsx # 신규: 레이아웃 TDD 테스트
```

## Architecture Decision Table

| 결정 | 고려한 옵션 | 선택 | 근거 | 프로젝트 구조 영향 | 테스트 용이성 |
|---|---|---|---|---|---|
| 음소거 아이콘 소스 | ① react-icons `RiVolumeMuteFill` ② 새 DTVolumeMuted 아이콘 | react-icons | 전체화면 토글 패턴과 일관성, 새 파일 불필요 | 변경 없음 | 높음 (클래스명/aria로 검증) |
| isMuted 파생 위치 | ① `TimerPage` 내 `volume === 0` ② `useTimerPageState` 인터페이스에 추가 | TimerPage 내 파생 | 파생값 추가 노출 불필요, 인터페이스 변경 최소화 | 변경 없음 | 높음 |
| 두 줄 레이아웃 방식 | ① flex-col + 개별 `<p>` ② `<br/>` 구분 | flex-col + 개별 `<p>` | 시맨틱, 각 줄 독립 스타일 적용 용이 | 없음 | 높음 (별도 요소로 쿼리) |
| 팀명 말줄임 | ① truncate를 팀명 줄에만 | truncate 팀명 줄만 | 토론자 줄은 항상 완전 표시 (FR-004) | 없음 | 높음 |
| 순서명 정렬 | ① text-center 추가 ② 현행 유지 | text-center 추가 | 한/영 모두 일관 중앙 정렬 보장 | 없음 | 높음 |

## TDD Implementation Order

### RED 단계 (테스트 먼저 작성)

**우선순위 1: NormalTimer 컴포넌트 (component 레이어)**

```
NormalTimer.test.tsx 작성 (모두 RED):
1. 팀명과 토론자가 각각 독립된 DOM 요소로 분리되는지
2. 팀명 줄에 truncate 클래스가 있는지
3. 토론자 줄에 truncate 클래스가 없는지
4. 팀명만 있을 때 토론자 요소가 미렌더링되는지
5. 팀명, 토론자 모두 없을 때 아이콘 영역 미렌더링되는지
6. h1(순서명)에 text-center 클래스가 있는지
```

**우선순위 2: TimerPage 페이지 (page 레이어)**

```
TimerPage.test.tsx 작성 (모두 RED):
1. volume > 0일 때 음소거 아이콘이 없는지
2. volume === 0일 때 음소거 아이콘이 표시되는지
3. 볼륨을 0으로 변경하면 헤더 아이콘이 즉시 음소거로 변경되는지
```

### GREEN 단계 (최소 구현)

```
NormalTimer.tsx 수정:
- flex-row → flex-col 변경
- 팀명 p 태그 분리 + truncate 유지
- 토론자 p 태그 분리
- h1에 text-center 추가

TimerPage.tsx 수정:
- volume === 0 조건으로 RiVolumeMuteFill / DTVolume 분기
```

### REFACTOR 단계

```
- 불필요한 className 정리
- 테스트 가독성 개선
```

## Complexity Tracking

> Constitution 위반 없음 — 이 섹션은 해당 없음
Loading
Loading