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
4 changes: 4 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@

-

### 실행화면

-

## 테스트

- [ ] 로컬 실행 확인
Expand Down
9 changes: 9 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"typescript.preferences.importModuleSpecifier": "non-relative",
"javascript.preferences.importModuleSpecifier": "non-relative"
}
10 changes: 10 additions & 0 deletions public/images/header/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes
File renamed without changes
File renamed without changes
9 changes: 9 additions & 0 deletions public/images/landing/bg.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions public/images/landing/hero-bg.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed public/landing/hero-bg.png
Binary file not shown.
15 changes: 15 additions & 0 deletions src/app/workspaces/[workspaceId]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// 워크스페이스 공통 사이드바와 헤더를 적용하는 라우트 레이아웃입니다.
import { WorkspaceShell } from '@/widgets/workspace-shell';

interface WorkspaceLayoutProps {
children: React.ReactNode;
params: Promise<{
workspaceId: string;
}>;
}

export default async function WorkspaceLayout({ children, params }: WorkspaceLayoutProps) {
const { workspaceId } = await params;

return <WorkspaceShell workspaceId={workspaceId}>{children}</WorkspaceShell>;
}
14 changes: 14 additions & 0 deletions src/app/workspaces/[workspaceId]/work-schedule/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// 워크스페이스 근무 일정 페이지의 라우트 진입점입니다.
import { WorkScheduleView } from '@/views/store-operation/work-schedule';

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

export default async function WorkSchedulePage({ params }: WorkSchedulePageProps) {
const { workspaceId } = await params;

return <WorkScheduleView workspaceId={workspaceId} />;
}
15 changes: 15 additions & 0 deletions src/entities/work-schedule/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// 근무 일정 도메인 타입, 목업 설정, 헬퍼 함수의 공개 API입니다.
export type {
WorkScheduleConfig,
WorkScheduleEntry,
WorkShiftColor,
WorkShiftOption,
} from './model/work-schedule.types';
export type { WeekdayKey } from './model/weekdays';
export { weekdays } from './model/weekdays';
export { countSchedulesByWeekday } from './lib/count-schedules-by-weekday';
export { createInitialWorkSchedule } from './lib/create-initial-work-schedule';
export { getDefaultWorkShiftOption } from './lib/get-default-work-shift-option';
export { getWorkMembersByWeekday } from './lib/get-work-members-by-weekday';
export { getNextWorkShiftOption } from './lib/get-next-work-shift-option';
export { mockWorkScheduleConfig } from './model/mock-work-schedule-config';
31 changes: 31 additions & 0 deletions src/entities/work-schedule/lib/count-schedules-by-weekday.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// 특정 요일의 각 근무 옵션에 몇 명의 멤버가 배정되어 있는지 계산합니다.
import type { WeekdayKey } from '../model/weekdays';
import type { WorkScheduleConfig, WorkScheduleEntry } from '../model/work-schedule.types';

interface CountSchedulesByWeekdayParams {
schedule: WorkScheduleEntry[];
config: WorkScheduleConfig;
weekday: WeekdayKey;
}

export function countSchedulesByWeekday({
schedule,
config,
weekday,
}: CountSchedulesByWeekdayParams): Record<string, number> {
const counts = Object.fromEntries(config.shifts.map((shift) => [shift.id, 0]));

const uniqueEntries = new Map<string, WorkScheduleEntry>();

schedule
.filter((entry) => entry.weekday === weekday)
.forEach((entry) => {
uniqueEntries.set(`${entry.userId}-${entry.weekday}`, entry);
});

uniqueEntries.forEach((entry) => {
counts[entry.shiftOptionId] = (counts[entry.shiftOptionId] ?? 0) + 1;
});

return counts;
}
28 changes: 28 additions & 0 deletions src/entities/work-schedule/lib/create-initial-work-schedule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// 모든 워크스페이스 멤버에 대한 기본 요일별 근무 일정을 생성합니다.
import type { WorkspaceMember } from '@/entities/workspace-member';
import { getDefaultWorkShiftOption } from './get-default-work-shift-option';
import { weekdays } from '../model/weekdays';
import type { WorkScheduleConfig, WorkScheduleEntry } from '../model/work-schedule.types';

interface CreateInitialWorkScheduleParams {
workspaceId: string;
members: WorkspaceMember[];
config: WorkScheduleConfig;
}

export function createInitialWorkSchedule({
workspaceId,
members,
config,
}: CreateInitialWorkScheduleParams): WorkScheduleEntry[] {
const defaultShift = getDefaultWorkShiftOption(config.shifts);

return members.flatMap((member) =>
weekdays.map((weekday) => ({
workspaceId,
userId: member.userId,
weekday: weekday.key,
shiftOptionId: defaultShift.id,
})),
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// 기본 셀 값으로 첫 번째 근무 중인 근무 옵션을 선택합니다.
import type { WorkShiftOption } from '../model/work-schedule.types';

export function getDefaultWorkShiftOption(shifts: WorkShiftOption[]): WorkShiftOption {
const firstWorkingShift = shifts.find((shift) => !shift.isOff);

return firstWorkingShift ?? shifts[0];
}
17 changes: 17 additions & 0 deletions src/entities/work-schedule/lib/get-next-work-shift-option.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// 설정된 배열 순서에 따라 다음 근무 옵션을 반환합니다.
import type { WorkShiftOption } from '../model/work-schedule.types';

interface GetNextWorkShiftOptionParams {
shifts: WorkShiftOption[];
currentShiftOptionId: string;
}

export function getNextWorkShiftOption({
shifts,
currentShiftOptionId,
}: GetNextWorkShiftOptionParams): WorkShiftOption {
const currentIndex = shifts.findIndex((shift) => shift.id === currentShiftOptionId);
const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % shifts.length;

return shifts[nextIndex];
}
Comment on lines +9 to +17

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== target file ==\n'
cat -n src/entities/work-schedule/lib/get-next-work-shift-option.ts

printf '\n== related helper search ==\n'
rg -n "getDefaultWorkShiftOption|type WorkShiftOption|interface WorkShiftOption|WorkShiftOption" src/entities/work-schedule -S

printf '\n== related files list ==\n'
fd -a "work-shift-option|work-schedule" src/entities/work-schedule

Repository: TeampleRun/syncly

Length of output: 2867


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== work-schedule.types ==\n'
cat -n src/entities/work-schedule/model/work-schedule.types.ts

printf '\n== get-default-work-shift-option ==\n'
cat -n src/entities/work-schedule/lib/get-default-work-shift-option.ts

printf '\n== create-initial-work-schedule ==\n'
cat -n src/entities/work-schedule/lib/create-initial-work-schedule.ts

Repository: TeampleRun/syncly

Length of output: 2375


shifts 가드를 추가하세요. shifts가 비어 있으면 shifts[nextIndex]undefined가 되어 WorkShiftOption 반환 계약을 깨뜨립니다. getDefaultWorkShiftOption도 같은 문제가 있으니 두 헬퍼 모두 빈 배열을 먼저 처리해야 합니다.

🤖 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/work-schedule/lib/get-next-work-shift-option.ts` around lines 9
- 17, Add an empty-array guard in getNextWorkShiftOption so it never returns
shifts[nextIndex] when shifts is empty, since that breaks the WorkShiftOption
contract. Check shifts before using findIndex/currentIndex and decide on a safe
fallback or explicit error path, and apply the same fix to
getDefaultWorkShiftOption because it has the same empty-shifts issue. Use the
symbols getNextWorkShiftOption and getDefaultWorkShiftOption to locate both
helpers.

28 changes: 28 additions & 0 deletions src/entities/work-schedule/lib/get-work-members-by-weekday.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// 휴무 옵션을 제외하여 특정 요일에 근무 중인 멤버를 판별합니다.
import type { WorkspaceMember } from '@/entities/workspace-member';
import type { WeekdayKey } from '../model/weekdays';
import type { WorkScheduleConfig, WorkScheduleEntry } from '../model/work-schedule.types';

interface GetWorkMembersByWeekdayParams {
schedule: WorkScheduleEntry[];
members: WorkspaceMember[];
config: WorkScheduleConfig;
weekday: WeekdayKey;
}

export function getWorkMembersByWeekday({
schedule,
members,
config,
weekday,
}: GetWorkMembersByWeekdayParams): WorkspaceMember[] {
const offShiftIds = new Set(config.shifts.filter((shift) => shift.isOff).map((shift) => shift.id));

const workingUserIds = new Set(
schedule
.filter((entry) => entry.weekday === weekday && !offShiftIds.has(entry.shiftOptionId))
.map((entry) => entry.userId),
);

return members.filter((member) => workingUserIds.has(member.userId));
}
39 changes: 39 additions & 0 deletions src/entities/work-schedule/model/mock-work-schedule-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// 워크스페이스별 설정이 저장되기 전까지 사용하는 기본 목업 근무 설정입니다.
import type { WorkScheduleConfig } from './work-schedule.types';

export const mockWorkScheduleConfig: WorkScheduleConfig = {
shifts: [
{
id: 'shift-open',
name: '오픈',
startTime: '09:00',
endTime: '14:00',
color: 'sky',
isOff: false,
},
{
id: 'shift-middle',
name: '미들',
startTime: '14:00',
endTime: '19:00',
color: 'violet',
isOff: false,
},
{
id: 'shift-close',
name: '마감',
startTime: '19:00',
endTime: '24:00',
color: 'amber',
isOff: false,
},
Comment on lines +22 to +29

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

date-fns parse "24:00" invalid time HH:mm

💡 Result:

In date-fns, the handling of "24:00" depends on the parsing function used [1][2][3]. For the parse function (which requires a format string), the "HH" token supports values from 00 to 23 [4][5]. Consequently, passing "24:00" with a format string like "HH:mm" will result in an "Invalid Date" because 24 is outside the supported range for that token [4][5]. For the parseISO function, the behavior is different because it is designed to follow the ISO 8601 standard [2][3]. According to ISO 8601, "24:00" is a valid representation of the end of a calendar day (equivalent to 00:00 of the following day) [2]. date-fns explicitly includes logic in parseISO to handle "24:00" correctly by treating it as midnight of the next day [2][3][6]. In summary: - parse(..., "HH:mm",...) treats 24 as invalid [4][5]. - parseISO(...) accepts "24:00" as a valid ISO 8601 time string [2][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## file outline\n'
ast-grep outline src/entities/work-schedule/model/mock-work-schedule-config.ts --view expanded || true

printf '\n## file contents\n'
cat -n src/entities/work-schedule/model/mock-work-schedule-config.ts | sed -n '1,120p'

printf '\n## search for endTime usage and time parsing\n'
rg -n "endTime|startTime|24:00|parseISO|parse\\(|format\\(|HH:mm|time" src | sed -n '1,220p'

Repository: TeampleRun/syncly

Length of output: 4027


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## related files\n'
fd -a ".*work-schedule.*|.*schedule.*" src || true

Repository: TeampleRun/syncly

Length of output: 1043


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## related files by exact name\n'
git ls-files 'src/**' | rg 'work-schedule|schedule' || true

Repository: TeampleRun/syncly

Length of output: 1270


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## work-schedule types\n'
cat -n src/entities/work-schedule/model/work-schedule.types.ts | sed -n '1,120p'

printf '\n## settings panel\n'
cat -n src/features/manage-work-schedule/ui/WorkShiftSettingsPanel.tsx | sed -n '1,220p'

printf '\n## board and legend\n'
cat -n src/features/manage-work-schedule/ui/WorkScheduleBoard.tsx | sed -n '1,220p'
printf '\n---\n'
cat -n src/features/manage-work-schedule/ui/WorkShiftLegend.tsx | sed -n '1,220p'

printf '\n## create initial config\n'
cat -n src/entities/work-schedule/lib/create-initial-work-schedule.ts | sed -n '1,220p'

printf '\n## find any time validation/parsing libraries around schedule\n'
rg -n "parseISO|parse\\(|isValid|HH:mm|type=\"time\"|endTime|startTime" src/entities src/features src/views | sed -n '1,240p'

Repository: TeampleRun/syncly

Length of output: 21847


🌐 Web query:

HTML input type=time 24:00 validity

💡 Result:

In HTML, the value 24:00 is not a valid time string for an element [1]. According to the HTML specification, a valid time string uses a 24-hour clock where the hour must be in the range 00 through 23 [1]. The value 00:00 represents midnight at the beginning of a day, and there is no representation of "end of day" as 24:00 within the standard time input format [1][2]. If you attempt to set the value attribute or the min/max attributes to 24:00, it will not be recognized as a valid time [3][4]. Consequently, if a browser performs client-side validation on an , it will treat 24:00 as invalid [4]. To handle intervals that include midnight or the end of a day, developers typically use a checkbox to indicate "entire day" or alternative input methods (such as text inputs with custom validation) [2][5].

Citations:


24:00 대신 23:59 또는 자정 종료 표현을 써 주세요.
WorkShiftSettingsPaneltype="time" 입력은 24:00을 허용하지 않아, 이 값은 수정 UI에서 제대로 표시·편집되지 않습니다.

🤖 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/work-schedule/model/mock-work-schedule-config.ts` around lines
22 - 29, The mock shift config uses an invalid endTime value of 24:00 for the
shift-close entry, which the WorkShiftSettingsPanel time input cannot display or
edit. Update the shift-close object in mock-work-schedule-config so endTime uses
23:59 or a proper midnight-end representation consistent with the rest of the
schedule model, keeping the name/id values unchanged.

{
id: 'shift-off',
name: '휴무',
startTime: null,
endTime: null,
color: 'slate',
isOff: true,
},
],
};
12 changes: 12 additions & 0 deletions src/entities/work-schedule/model/weekdays.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// 첫 번째 근무 일정 목업에서 사용하는 요일 컬럼입니다.
export const weekdays = [
{ key: 'monday', label: '월' },
{ key: 'tuesday', label: '화' },
{ key: 'wednesday', label: '수' },
{ key: 'thursday', label: '목' },
{ key: 'friday', label: '금' },
{ key: 'saturday', label: '토' },
{ key: 'sunday', label: '일' },
] as const;

export type WeekdayKey = (typeof weekdays)[number]['key'];
24 changes: 24 additions & 0 deletions src/entities/work-schedule/model/work-schedule.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// 목업 근무 일정 모듈의 설정과 항목에 대한 핵심 타입입니다.
import type { WeekdayKey } from './weekdays';

export type WorkShiftColor = 'sky' | 'violet' | 'amber' | 'slate' | 'emerald' | 'rose';

export interface WorkShiftOption {
id: string;
name: string;
startTime: string | null;
endTime: string | null;
color: WorkShiftColor;
isOff: boolean;
}

export interface WorkScheduleConfig {
shifts: WorkShiftOption[];
}

export interface WorkScheduleEntry {
workspaceId: string;
userId: string;
weekday: WeekdayKey;
shiftOptionId: string;
}
4 changes: 4 additions & 0 deletions src/entities/workspace-member/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// 목업 워크스페이스 멤버 데이터와 타입의 공개 API입니다.
export type { WorkspaceMember } from './model/workspace-member.types';
export { mockCurrentWorkspaceMember } from './model/mock-current-workspace-member';
export { mockWorkspaceMembers } from './model/mock-workspace-members';
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// 인증/멤버 API 연결 전 shell 푸터와 헤더에 표시하는 현재 멤버 목업입니다.
import type { WorkspaceMember } from './workspace-member.types';

export const mockCurrentWorkspaceMember: WorkspaceMember = {
workspaceId: 'test',
userId: 'user-1',
workspaceNickname: '김민서',
avatarLabel: '김',
role: 'owner',
};
47 changes: 47 additions & 0 deletions src/entities/workspace-member/model/mock-workspace-members.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { WorkspaceMember } from './workspace-member.types';

// mock-member data, 실제 서버와 연동이 되면 수정될 예정
export const mockWorkspaceMembers: WorkspaceMember[] = [
{
workspaceId: 'store-workspace',
userId: 'user-1',
workspaceNickname: '김민서',
avatarLabel: '김',
role: 'owner',
},
{
workspaceId: 'store-workspace',
userId: 'user-2',
workspaceNickname: '이준혁',
avatarLabel: '이',
role: 'member',
},
{
workspaceId: 'store-workspace',
userId: 'user-3',
workspaceNickname: '박소연',
avatarLabel: '박',
role: 'member',
},
{
workspaceId: 'store-workspace',
userId: 'user-4',
workspaceNickname: '최다은',
avatarLabel: '최',
role: 'member',
},
{
workspaceId: 'store-workspace',
userId: 'user-5',
workspaceNickname: '정우진',
avatarLabel: '정',
role: 'member',
},
{
workspaceId: 'store-workspace',
userId: 'user-6',
workspaceNickname: '이서영',
avatarLabel: '이',
role: 'member',
},
];
8 changes: 8 additions & 0 deletions src/entities/workspace-member/model/workspace-member.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Supabase 데이터가 연결되기 전까지 사용하는 목업 일정용 워크스페이스 멤버 형태입니다.
export interface WorkspaceMember {
workspaceId: string;
userId: string;
workspaceNickname: string;
avatarLabel: string;
role: 'owner' | 'member';
}
3 changes: 3 additions & 0 deletions src/entities/workspace/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// 목업 워크스페이스 데이터와 타입의 공개 API입니다.
export type { Workspace } from './model/workspace.types';
export { mockWorkspace } from './model/mock-workspace';
8 changes: 8 additions & 0 deletions src/entities/workspace/model/mock-workspace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// 워크스페이스 API 연결 전 사이드바에 표시하는 현재 워크스페이스 목업입니다.
import type { Workspace } from './workspace.types';

export const mockWorkspace: Workspace = {
id: 'test',
name: '카페 그레이 운영',
purpose: 'store-operation',
};
6 changes: 6 additions & 0 deletions src/entities/workspace/model/workspace.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Supabase 워크스페이스 데이터 연결 전 shell에서 사용하는 워크스페이스 타입입니다.
export interface Workspace {
id: string;
name: string;
purpose: 'store-operation' | 'team-project' | 'side-project';
}
2 changes: 2 additions & 0 deletions src/features/manage-work-schedule/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// 근무 일정 관리 기능의 공개 API입니다.
export { WorkScheduleBoard } from './ui/WorkScheduleBoard';
Loading