From 4ff746638cb6c5fc8c76c53cfd61035d4be96b6e Mon Sep 17 00:00:00 2001 From: ttipo Date: Tue, 23 Jun 2026 12:45:52 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EC=9D=BC=EC=A0=95=20=EC=A3=BC?= =?UTF-8?q?=EA=B0=84/=EC=9B=94=EA=B0=84=20=EB=B7=B0=20=EC=A0=84=ED=99=98?= =?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 상단 토글(주간/월간)로 보기 전환. 월간은 전통 달력 그리드(주×요일). - scheduleDate: firstOfMonth/addMonths/monthLabel/monthGridWeeks(+MonthDay) 추가 - schedule.store: viewMode + anchor 기반으로 주간/월간 범위·라벨·네비 일원화 (goPrev/goNext 가 모드별로 주/월 이동, setViewMode 시 범위 재조회) - ScheduleMonthGrid: 주 단위 다중일 막대(레인 패킹) + 초과 '+N', 카테고리 색·유형 필터·오늘/주말/이전·다음달 표기, 막대 클릭 시 상세 패널 - SchedulePage: SegmentedTabs 토글 + 주간/월간 조건부 렌더 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../components/schedule/ScheduleMonthGrid.vue | 295 ++++++++++++++++++ frontend/src/pages/relay/SchedulePage.vue | 35 ++- frontend/src/shared/utils/scheduleDate.ts | 60 ++++ frontend/src/stores/schedule.store.ts | 83 +++-- 4 files changed, 447 insertions(+), 26 deletions(-) create mode 100644 frontend/src/components/schedule/ScheduleMonthGrid.vue diff --git a/frontend/src/components/schedule/ScheduleMonthGrid.vue b/frontend/src/components/schedule/ScheduleMonthGrid.vue new file mode 100644 index 0000000..9be2302 --- /dev/null +++ b/frontend/src/components/schedule/ScheduleMonthGrid.vue @@ -0,0 +1,295 @@ + + + + + diff --git a/frontend/src/pages/relay/SchedulePage.vue b/frontend/src/pages/relay/SchedulePage.vue index f3497a1..9d5dc05 100644 --- a/frontend/src/pages/relay/SchedulePage.vue +++ b/frontend/src/pages/relay/SchedulePage.vue @@ -1,13 +1,15 @@ @@ -208,7 +284,7 @@ const laidWeeks = computed(() => min-height: 6rem; display: grid; grid-template-columns: repeat(7, 1fr); - grid-template-rows: 1.625rem repeat(3, 1.375rem) 1fr; + grid-template-rows: 1.625rem repeat(3, 1.55rem) 1fr; row-gap: 0.125rem; border-top: 1px solid var(--border); } @@ -259,7 +335,7 @@ const laidWeeks = computed(() => z-index: 1; margin: 0 0.25rem; padding: 0 0.4375rem; - height: 1.25rem; + height: 1.45rem; display: flex; align-items: center; border: none; @@ -287,9 +363,85 @@ const laidWeeks = computed(() => .mg-more { z-index: 1; align-self: start; + justify-self: start; margin: 0 0.4375rem; + padding: 0.0625rem 0.25rem; + border: none; + background: transparent; + border-radius: 0.25rem; + font-family: inherit; font-size: 0.6875rem; font-weight: 700; color: var(--text-3); + cursor: pointer; +} +.mg-more:hover { + background: #eceef1; + color: var(--text-2); +} + +/* 날짜별 전체 일정 팝오버 */ +.mg-pop { + position: fixed; + z-index: 1000; + width: 15rem; + max-height: 18rem; + overflow-y: auto; + background: #fff; + border: 1px solid var(--border); + border-radius: 0.625rem; + box-shadow: var(--shadow-pop); + padding: 0.375rem; +} +.mg-pop-h { + padding: 0.375rem 0.5rem 0.5rem; + font-size: 0.75rem; + font-weight: 700; + color: var(--text-3); +} +.mg-pop-row { + display: flex; + align-items: center; + gap: 0.5rem; + width: 100%; + padding: 0.4375rem 0.5rem; + border: none; + background: transparent; + border-radius: var(--radius-sm); + cursor: pointer; + font-family: inherit; + text-align: left; +} +.mg-pop-row:hover { + background: #f5f6f8; +} +.mg-pop-row.sel { + background: var(--accent-weak); +} +.mg-pop-dot { + width: 0.5rem; + height: 0.5rem; + border-radius: 50%; + flex-shrink: 0; +} +.mg-pop-tm { + font-size: 0.719rem; + font-weight: 600; + color: var(--text-3); + flex-shrink: 0; +} +.mg-pop-tt { + font-size: 0.8125rem; + font-weight: 600; + color: var(--text); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.mg-pop-empty { + padding: 0.625rem 0.5rem; + font-size: 0.75rem; + color: var(--text-3); + text-align: center; } diff --git a/frontend/src/pages/relay/SchedulePage.vue b/frontend/src/pages/relay/SchedulePage.vue index 9d5dc05..962ab2d 100644 --- a/frontend/src/pages/relay/SchedulePage.vue +++ b/frontend/src/pages/relay/SchedulePage.vue @@ -150,6 +150,27 @@ async function onDeleteCategory(id: string): Promise { @update:model-value="store.setViewMode" />
+ { { { .spacer { flex: 1; } +/* '내 일정' 토글 — 정렬/필터 pill 과 동일 외형, 활성 시 강조색 */ +.mine-btn { + display: inline-flex; + align-items: center; + gap: 0.4375rem; + height: 2.25rem; + padding: 0 0.75rem; + border: 1px solid var(--border-strong); + border-radius: var(--radius); + background: #fff; + font-family: inherit; + font-size: 0.8125rem; + font-weight: 600; + color: var(--text-2); + cursor: pointer; + white-space: nowrap; +} +.mine-btn:hover { + background: #f9fafb; +} +.mine-btn svg { + width: 0.9375rem; + height: 0.9375rem; + color: var(--text-3); +} +.mine-btn.on { + border-color: var(--accent); + background: var(--accent-weak); + color: var(--accent); +} +.mine-btn.on svg { + color: var(--accent); +} /* '새 일정' 버튼은 전역 .btn / .btn.primary 표준(다른 화면의 주요 버튼과 동일)을 그대로 사용 */ .content-row { flex: 1; diff --git a/frontend/src/stores/schedule.store.ts b/frontend/src/stores/schedule.store.ts index acad015..60b8160 100644 --- a/frontend/src/stores/schedule.store.ts +++ b/frontend/src/stores/schedule.store.ts @@ -1,6 +1,7 @@ import { computed, ref } from 'vue' import { defineStore } from 'pinia' import { useSchedule } from '@/composables/useSchedule' +import { useAuthStore } from './auth.store' import { addDays, addMonths, @@ -27,12 +28,15 @@ export type ScheduleViewMode = 'week' | 'month' // 일정 스토어 — 카테고리/일정/참석자 + 주간/월간 네비 상태 + CRUD export const useScheduleStore = defineStore('schedule', () => { const api = useSchedule() + const authStore = useAuthStore() const categories = ref([]) const events = ref([]) const people = ref([]) // 활성(표시) 카테고리 id 집합 — 유형 필터 const activeTypeIds = ref>(new Set()) + // '내 일정만' 보기 — 참석자/전직원/작성자 기준 + const mineOnly = ref(false) // 보기 모드(주간/월간) + 기준 날짜(이 날짜가 속한 주/월을 표시) const viewMode = ref('week') const anchor = ref(todayIso()) @@ -64,10 +68,22 @@ export const useScheduleStore = defineStore('schedule', () => { : monthLabel(anchor.value), ) - // 카테고리별 현재 범위 일정 수(필터 드롭다운 표기용) + // '내 일정' 판정 — 참석자 기준(전 직원 대상은 전원 포함이므로 내 일정에 포함) + const myId = computed(() => authStore.user?.id ?? null) + function isMine(e: ApiScheduleEvent): boolean { + if (!myId.value) return false + return e.allHands || e.attendees.some((a) => a.id === myId.value) + } + // 화면 표시 대상 일정 — '내 일정만' 토글 적용(카테고리 필터는 뷰 컴포넌트가 처리) + const displayEvents = computed(() => + mineOnly.value ? events.value.filter(isMine) : events.value, + ) + + // 카테고리별 현재 범위 일정 수(필터 드롭다운 표기용) — 표시 대상 기준 const counts = computed>(() => { const c: Record = {} - for (const e of events.value) c[e.categoryId] = (c[e.categoryId] || 0) + 1 + for (const e of displayEvents.value) + c[e.categoryId] = (c[e.categoryId] || 0) + 1 return c }) @@ -133,6 +149,9 @@ export const useScheduleStore = defineStore('schedule', () => { ? new Set(categories.value.map((c) => c.id)) : new Set() } + function toggleMineOnly(): void { + mineOnly.value = !mineOnly.value + } // --- 일정 CRUD --- async function createEvent(payload: ScheduleEventPayload): Promise { @@ -188,8 +207,10 @@ export const useScheduleStore = defineStore('schedule', () => { return { categories, events, + displayEvents, people, activeTypeIds, + mineOnly, viewMode, weekStart, loading, @@ -205,6 +226,7 @@ export const useScheduleStore = defineStore('schedule', () => { goToday, toggleType, setAllTypes, + toggleMineOnly, createEvent, updateEvent, deleteEvent,