Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/app/workspaces/[workspaceId]/calendar/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { CalendarPage } from '@/views/calendar';

interface WorkspaceCalendarPageProps {
params: Promise<{
workspaceId: string;
}>;
}

export default async function WorkspaceCalendarPage({
params,
}: WorkspaceCalendarPageProps) {
const { workspaceId } = await params;

return <CalendarPage workspaceId={workspaceId} />;
}
7 changes: 7 additions & 0 deletions src/entities/calendar-event/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export { getMockCalendarEventsByWorkspaceId } from './model/mock-calendar-events-by-workspace';
export type {
CalendarEvent,
CalendarEventColor,
CalendarEventFormValues,
} from './model/calendar-event.types';
export { CalendarEventChip } from './ui/CalendarEventChip';
16 changes: 16 additions & 0 deletions src/entities/calendar-event/model/calendar-event.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export type CalendarEventColor = 'violet' | 'purple' | 'blue' | 'green' | 'amber' | 'coral' | 'pink';

export interface CalendarEvent {
id: string;
workspaceId: string;
title: string;
date: string;
time: string | null;
color: CalendarEventColor;
}

export interface CalendarEventFormValues {
title: string;
time: string;
color: CalendarEventColor;
}
142 changes: 142 additions & 0 deletions src/entities/calendar-event/model/mock-calendar-events-by-workspace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import type { CalendarEvent } from './calendar-event.types';

const mockCalendarEventsByWorkspaceId: Record<string, CalendarEvent[]> = {
test: [
{
id: 'calendar-event-1',
workspaceId: 'test',
title: 'API 명세 마감',
date: '2025-07-02',
time: null,
color: 'violet',
},
{
id: 'calendar-event-2',
workspaceId: 'test',
title: '백로그 정리',
date: '2025-07-02',
time: '오후 1:00',
color: 'blue',
},
{
id: 'calendar-event-3',
workspaceId: 'test',
title: '와이어프레임',
date: '2025-07-03',
time: null,
color: 'purple',
},
{
id: 'calendar-event-4',
workspaceId: 'test',
title: '스프린트',
date: '2025-07-03',
time: null,
color: 'blue',
},
{
id: 'calendar-event-5',
workspaceId: 'test',
title: '사용자 리서치',
date: '2025-07-05',
time: null,
color: 'blue',
},
{
id: 'calendar-event-6',
workspaceId: 'test',
title: '회의 안건 정리',
date: '2025-07-05',
time: '오전 10:00',
color: 'amber',
},
{
id: 'calendar-event-7',
workspaceId: 'test',
title: '랜딩 디자인',
date: '2025-07-08',
time: null,
color: 'violet',
},
{
id: 'calendar-event-8',
workspaceId: 'test',
title: '카피 문구 검토',
date: '2025-07-08',
time: '오후 2:00',
color: 'purple',
},
{
id: 'calendar-event-9',
workspaceId: 'test',
title: 'MVP 완성',
date: '2025-07-14',
time: null,
color: 'green',
},
{
id: 'calendar-event-10',
workspaceId: 'test',
title: '중간 점검 회의',
date: '2025-07-14',
time: '오후 4:00',
color: 'blue',
},
{
id: 'calendar-event-11',
workspaceId: 'test',
title: 'QA 시작',
date: '2025-07-22',
time: null,
color: 'coral',
},
{
id: 'calendar-event-12',
workspaceId: 'test',
title: '버그 우선순위 정리',
date: '2025-07-22',
time: '오전 11:00',
color: 'pink',
},
{
id: 'calendar-event-13',
workspaceId: 'test',
title: '배포 준비',
date: '2025-07-28',
time: null,
color: 'violet',
},
{
id: 'calendar-event-14',
workspaceId: 'test',
title: '릴리즈 체크리스트',
date: '2025-07-28',
time: '오후 1:00',
color: 'green',
},
{
id: 'calendar-event-15',
workspaceId: 'test',
title: '최종 발표 리허설',
date: '2025-07-30',
time: '오후 3:00',
color: 'violet',
},
{
id: 'calendar-event-16',
workspaceId: 'test',
title: '발표 자료 검수',
date: '2025-07-30',
time: '오후 5:00',
color: 'amber',
},
],
};

export function getMockCalendarEventsByWorkspaceId(workspaceId: string): CalendarEvent[] {
return (
mockCalendarEventsByWorkspaceId[workspaceId]?.map((event) => ({
...event,
})) ?? []
);
}
30 changes: 30 additions & 0 deletions src/entities/calendar-event/ui/CalendarEventChip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { cn } from '@/shared/lib/utils';
import type { CalendarEvent, CalendarEventColor } from '../model/calendar-event.types';

const colorClassNames: Record<CalendarEventColor, string> = {
violet: 'bg-[#6b5cff]',
purple: 'bg-[#7f5cff]',
blue: 'bg-[#2f7df6]',
green: 'bg-[#10b74a]',
amber: 'bg-[#ff9f0a]',
coral: 'bg-[#ff6565]',
pink: 'bg-[#eb2f96]',
};
Comment on lines +4 to +12

@coderabbitai coderabbitai Bot Jul 9, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

색상 매핑 불일치: 칩 색상이 선택 UI와 다릅니다.

colorClassNamespurple(#7f5cff)과 blue(#2f7df6) hex 값이 CalendarView.tsxcolorButtonClassNames/colorDotClassNames에서 사용하는 값(#8b5cf6, #3b82f6)과 다릅니다. 사용자가 모달에서 색상을 고르면 실제 칩에는 다른 색으로 렌더링됩니다. 색상 상수를 한 곳(예: calendar-event.types.ts 또는 공용 constants 파일)으로 통합하는 것을 권장합니다.

🎨 제안: 공유 색상 상수로 통합
-const colorClassNames: Record<CalendarEventColor, string> = {
-  violet: 'bg-[`#6b5cff`]',
-  purple: 'bg-[`#7f5cff`]',
-  blue: 'bg-[`#2f7df6`]',
-  green: 'bg-[`#10b74a`]',
-  amber: 'bg-[`#ff9f0a`]',
-  coral: 'bg-[`#ff6565`]',
-  pink: 'bg-[`#eb2f96`]',
-};
+// CalendarView.tsx의 colorButtonClassNames/colorDotClassNames와 동일한 값 사용
+const colorClassNames: Record<CalendarEventColor, string> = {
+  violet: 'bg-[`#6b5cff`]',
+  purple: 'bg-[`#8b5cf6`]',
+  blue: 'bg-[`#3b82f6`]',
+  green: 'bg-[`#10b74a`]',
+  amber: 'bg-[`#ff9f0a`]',
+  coral: 'bg-[`#ff6565`]',
+  pink: 'bg-[`#eb2f96`]',
+};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const colorClassNames: Record<CalendarEventColor, string> = {
violet: 'bg-[#6b5cff]',
purple: 'bg-[#7f5cff]',
blue: 'bg-[#2f7df6]',
green: 'bg-[#10b74a]',
amber: 'bg-[#ff9f0a]',
coral: 'bg-[#ff6565]',
pink: 'bg-[#eb2f96]',
};
const colorClassNames: Record<CalendarEventColor, string> = {
violet: 'bg-[`#6b5cff`]',
purple: 'bg-[`#8b5cf6`]',
blue: 'bg-[`#3b82f6`]',
green: 'bg-[`#10b74a`]',
amber: 'bg-[`#ff9f0a`]',
coral: 'bg-[`#ff6565`]',
pink: 'bg-[`#eb2f96`]',
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/entities/calendar-event/ui/CalendarEventChip.tsx` around lines 4 - 12,
The calendar event chip color mapping is inconsistent with the color picker
values, so selected colors render differently in the chip UI. Update the
`colorClassNames` mapping in `CalendarEventChip` to use the same hex values as
`CalendarView`’s `colorButtonClassNames` and `colorDotClassNames`, and
preferably move these shared color values into a common constant source such as
`calendar-event.types.ts` or a shared constants file. Keep the
`CalendarEventColor` keys aligned so both the modal selection and chip rendering
use the same definitions.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

CalendarEventColor 색상 매핑이 CalendarView와 CalendarEventChip에 중복 정의되어 있고, purple/blue의 hex 값이 서로 달라 선택 UI와 실제 칩 색상이 다르게 보일 수 있을 것 같습니다.
색상 매핑을 공용 constants로 분리해서 두 컴포넌트가 동일한 값을 참조하도록 통일하면 좋겠습니다!

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
ast-grep outline src/features/manage-calendar/ui/CalendarView.tsx --items all
echo "---"
rg -n "ClassNames|#[0-9a-fA-F]{6}" src/features/manage-calendar/ui/CalendarView.tsx src/entities/calendar-event/ui/CalendarEventChip.tsx

Length of output: 5819


🏁 Script executed:

rg -n "colorButtonClassNames\[|colorDotClassNames\[|colorClassNames\[" src/features/manage-calendar/ui/CalendarView.tsx src/entities/calendar-event/ui/CalendarEventChip.tsx

Length of output: 721


Confirmed — there are exactly 3 usage sites to update:

  • src/entities/calendar-event/ui/CalendarEventChip.tsx:23colorClassNames[event.color]
  • src/features/manage-calendar/ui/CalendarView.tsx:267colorDotClassNames[calendarEvent.color]
  • src/features/manage-calendar/ui/CalendarView.tsx:369colorButtonClassNames[color]

The plan already covers all three: replace each with CALENDAR_EVENT_COLOR_CLASS_NAMES[...] after importing the new shared constant from the entities/calendar-event layer (respecting the FSD feature → entity import direction). No other usages were found, so the migration is contained to these two files plus the new constants file and barrel export.


✏️ Learnings added
Learnt from: seongjinss555
Repo: TeampleRun/syncly PR: 35
File: src/entities/calendar-event/ui/CalendarEventChip.tsx:4-12
Timestamp: 2026-07-09T07:24:49.606Z
Learning: In `syncly` repo (Next.js/TypeScript, FSD architecture), calendar event color-to-Tailwind-class mappings previously existed duplicated in `src/entities/calendar-event/ui/CalendarEventChip.tsx` (as `colorClassNames`) and `src/features/manage-calendar/ui/CalendarView.tsx` (as `colorButtonClassNames` and `colorDotClassNames`), with mismatched hex values for `purple`/`blue`. Prefer defining shared `CalendarEventColor` class-name constants once in the `entities/calendar-event` layer (e.g. `calendar-event-colors.ts`) and importing them from `features/manage-calendar` to keep chip, button, and dot colors consistent, per FSD import direction (`feature → entity`).

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

칩, 색상 선택 버튼, 오른쪽 패널 점 표시가 각각 다른 hex 값을 보던 상태여서 색상 매핑을 calendar-event-colors.ts로 공통화했습니다.
이제 CalendarEventChip, CalendarView 모두 동일한 색상 상수를 사용합니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!


interface CalendarEventChipProps {
event: CalendarEvent;
}

export function CalendarEventChip({ event }: CalendarEventChipProps) {
return (
<div
className={cn(
'h-5 truncate rounded-full px-2 text-[10px] leading-5 font-semibold text-white',
colorClassNames[event.color],
)}
title={event.title}
>
{event.title}
</div>
);
}
5 changes: 5 additions & 0 deletions src/entities/workspace/model/mock-workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import { cache } from 'react';
import type { Workspace } from './workspace.types';

const mockWorkspacesById: Record<string, Workspace> = {
test: {
id: 'test',
name: '캡스톤 디자인 팀',
purpose: 'team-project',
},
'store-workspace': {
id: 'store-workspace',
name: '카페 그레이 운영',
Expand Down
1 change: 1 addition & 0 deletions src/features/manage-calendar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { CalendarView } from './ui/CalendarView';
40 changes: 40 additions & 0 deletions src/features/manage-calendar/model/calendar-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
export interface CalendarDayCell {
isoDate: string;
dayNumber: number;
isCurrentMonth: boolean;
}

function formatIsoDate(date: Date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');

return `${year}-${month}-${day}`;
}

export function createCalendarMonthLabel(currentMonth: Date) {
return `${currentMonth.getFullYear()}년 ${currentMonth.getMonth() + 1}월`;
}

export function createCalendarMonthGrid(currentMonth: Date): CalendarDayCell[] {
const year = currentMonth.getFullYear();
const monthIndex = currentMonth.getMonth();
const firstDayOfMonth = new Date(year, monthIndex, 1);
const startDayIndex = firstDayOfMonth.getDay();
const gridStartDate = new Date(year, monthIndex, 1 - startDayIndex);

return Array.from({ length: 35 }, (_, index) => {
const date = new Date(gridStartDate);
date.setDate(gridStartDate.getDate() + index);

return {
isoDate: formatIsoDate(date),
dayNumber: date.getDate(),
isCurrentMonth: date.getMonth() === monthIndex,
};
});
}
Comment on lines +19 to +36

@coderabbitai coderabbitai Bot Jul 9, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

월별 셀 개수를 35개로 고정하면 일부 월의 마지막 날짜가 그리드에서 잘립니다.

startDayIndex(월 시작 요일)와 daysInMonth(해당 월 일수) 조합에 따라 35개 셀(5주)로는 월 전체를 표현할 수 없는 경우가 있습니다. 예를 들어 2025년 8월은 금요일(startDayIndex=5)에 시작하고 31일이므로, 8월 31일이 인덱스 35에 위치하게 되어 배열(0~34) 밖으로 밀려나 렌더링되지 않습니다. 31일짜리 월이 금/토에 시작하거나 30일짜리 월이 토요일에 시작하는 모든 경우에 동일한 문제가 발생합니다.

🐛 셀 개수를 동적으로 계산하는 수정안
 export function createCalendarMonthGrid(currentMonth: Date): CalendarDayCell[] {
   const year = currentMonth.getFullYear();
   const monthIndex = currentMonth.getMonth();
   const firstDayOfMonth = new Date(year, monthIndex, 1);
   const startDayIndex = firstDayOfMonth.getDay();
   const gridStartDate = new Date(year, monthIndex, 1 - startDayIndex);
+  const daysInMonth = new Date(year, monthIndex + 1, 0).getDate();
+  const totalCells = Math.ceil((startDayIndex + daysInMonth) / 7) * 7;

-  return Array.from({ length: 35 }, (_, index) => {
+  return Array.from({ length: totalCells }, (_, index) => {
     const date = new Date(gridStartDate);
     date.setDate(gridStartDate.getDate() + index);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function createCalendarMonthGrid(currentMonth: Date): CalendarDayCell[] {
const year = currentMonth.getFullYear();
const monthIndex = currentMonth.getMonth();
const firstDayOfMonth = new Date(year, monthIndex, 1);
const startDayIndex = firstDayOfMonth.getDay();
const gridStartDate = new Date(year, monthIndex, 1 - startDayIndex);
return Array.from({ length: 35 }, (_, index) => {
const date = new Date(gridStartDate);
date.setDate(gridStartDate.getDate() + index);
return {
isoDate: formatIsoDate(date),
dayNumber: date.getDate(),
isCurrentMonth: date.getMonth() === monthIndex,
};
});
}
export function createCalendarMonthGrid(currentMonth: Date): CalendarDayCell[] {
const year = currentMonth.getFullYear();
const monthIndex = currentMonth.getMonth();
const firstDayOfMonth = new Date(year, monthIndex, 1);
const startDayIndex = firstDayOfMonth.getDay();
const gridStartDate = new Date(year, monthIndex, 1 - startDayIndex);
const daysInMonth = new Date(year, monthIndex + 1, 0).getDate();
const totalCells = Math.ceil((startDayIndex + daysInMonth) / 7) * 7;
return Array.from({ length: totalCells }, (_, index) => {
const date = new Date(gridStartDate);
date.setDate(gridStartDate.getDate() + index);
return {
isoDate: formatIsoDate(date),
dayNumber: date.getDate(),
isCurrentMonth: date.getMonth() === monthIndex,
};
});
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/manage-calendar/model/calendar-utils.ts` around lines 19 - 36,
createCalendarMonthGrid currently hardcodes a 35-cell month grid, which can
truncate the last day for months that need 6 weeks; update the CalendarDayCell
generation in createCalendarMonthGrid to compute the required cell count
dynamically from startDayIndex and the number of days in the month, and return
enough cells to cover the full calendar span. Keep the existing logic for
gridStartDate, isoDate, dayNumber, and isCurrentMonth, but replace the fixed
Array.from length with a calculated value that always includes the last day of
the month.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

혹시 모를 상황에 대비하여 동적으로 관리하는 것으로 수정하면 좋을 거 같습니다~

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The PR description image (Image 1) shows the Syncly calendar page for "2025년 7월" with a monthly grid (일~토), colored event chips in various day cells, and a right-side detail panel for "2025년 7월 30일" showing an empty-state message ("등록된 일정이 없어요").

The PR description image (Image 2) shows the same calendar with a modal overlay titled "2025년 7월 30일 일정 추가", containing fields for 일정 이름, 시간 (선택), and 색상 (color picker dots), with 취소/추가 buttons at the bottom.

@seongjinss555 네, 동의합니다! 말씀하신 대로 월 시작 요일과 해당 월의 일수에 따라 필요한 셀 개수를 동적으로 계산하는 방식이 더 안전합니다. 위에서 제안한 diff처럼 daysInMonthtotalCells를 계산해서 Array.from({ length: totalCells }, ...)로 바꾸시면 5주(35칸)로 부족한 달(예: 2025년 8월처럼 금요일 시작 + 31일)도 문제없이 렌더링됩니다. 반영해주시면 확인하겠습니다 🙂

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

월 시작 요일과 말일 조합에 따라 5주(35칸)로는 월 전체를 표현할 수 없는 경우가 있어서, 그리드 셀 수를 동적으로 계산하도록 수정했습니다.
이제 startDayIndex + daysInMonth > 35인 경우에는 42칸(6주)으로 렌더링되고, 그 외 월은 기존처럼 35칸을 유지합니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!


export function getInitialCalendarDate() {
return new Date(2025, 6, 1);
}
44 changes: 44 additions & 0 deletions src/features/manage-calendar/model/use-calendar-events-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use client';

import { create } from 'zustand';
import type { CalendarEvent } from '@/entities/calendar-event';

interface CalendarEventsStore {
calendarEventsByWorkspaceId: Record<string, CalendarEvent[]>;
initializeWorkspace: (workspaceId: string, events: CalendarEvent[]) => void;
addCalendarEvent: (workspaceId: string, event: CalendarEvent) => void;
removeCalendarEvent: (workspaceId: string, eventId: string) => void;
}

export const useCalendarEventsStore = create<CalendarEventsStore>((set) => ({
calendarEventsByWorkspaceId: {},
initializeWorkspace: (workspaceId, events) =>
set((state) => {
if (state.calendarEventsByWorkspaceId[workspaceId]) {
return state;
}

return {
calendarEventsByWorkspaceId: {
...state.calendarEventsByWorkspaceId,
[workspaceId]: events,
},
};
}),
addCalendarEvent: (workspaceId, event) =>
set((state) => ({
calendarEventsByWorkspaceId: {
...state.calendarEventsByWorkspaceId,
[workspaceId]: [...(state.calendarEventsByWorkspaceId[workspaceId] ?? []), event],
},
})),
removeCalendarEvent: (workspaceId, eventId) =>
set((state) => ({
calendarEventsByWorkspaceId: {
...state.calendarEventsByWorkspaceId,
[workspaceId]: (state.calendarEventsByWorkspaceId[workspaceId] ?? []).filter(
(event) => event.id !== eventId,
),
},
})),
}));
Loading