Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
c2e279f
feat: 날짜 겹침 로직
hamo-o Mar 3, 2025
8b2ed35
feat: 겹치는 시간대끼리 그룹핑
hamo-o Mar 3, 2025
8ea417c
refactor: 매직넘버 삭제
hamo-o Mar 6, 2025
1b186ae
feat: 현재 캘린더 드래그 중인지를 나타내는 isSelecting 필드 훅에 추가
hamo-o Jun 9, 2025
db0d5fc
fix: isSelecting일때는 카드 이벤트를 막아서 드래그 이벤트 버블링이 정상적으로 동작하도록 하기
hamo-o Jun 9, 2025
ac97ab8
fix: defaultVariants 추가
hamo-o Jun 10, 2025
ecd9110
chore: @vitest/browser, playwright 설치 및 vanilla extract 업데이트
hamo-o Jun 10, 2025
8b238c2
chore: vitest-browser-react 설치
hamo-o Jun 10, 2025
1423c1f
chore: 불필요한 테스팅 라이브러리 삭제
hamo-o Jun 10, 2025
a3b5ea2
chore: 정상적 모듈 인식을 위해 @vanilla-extract/vite-plugin 다운그레이드
hamo-o Jun 10, 2025
67672c4
chore: vitest browser를 위한 세팅
hamo-o Jun 10, 2025
4ea5b88
chore: vitest browser config 패키지 분리
hamo-o Jun 10, 2025
fcf9ecb
chore: vitest가 VE를 이해할 수 있도록 플러그인 세팅 추가
hamo-o Jun 10, 2025
478a22d
chore: 타입 추론을 위한 세팅 파일 추가
hamo-o Jun 10, 2025
21f8ff4
fix: react 문법 이해를 위한 플러그인 추가
hamo-o Jun 10, 2025
70e6f36
chore: 브라우저 크기 데스크탑으로 변경
hamo-o Jun 10, 2025
f3788c6
fix: 테스트 범위는 소비자 쪽에서 작성하도록 수정
hamo-o Jun 10, 2025
6d7495a
test: 캘린더 리스트 렌더링 시간
hamo-o Jun 10, 2025
f5bf185
chore: vitest-browser-react 대신 @testing-library 재설치
hamo-o Jun 11, 2025
55adc62
chore: react 루트로 의존성 이동, @testing-library/jest-dom 설치
hamo-o Jun 11, 2025
00088fd
fix: 빠진 react 플러그인 추가
hamo-o Jun 11, 2025
52e5495
chore: @testing-library/jest-dom setup
hamo-o Jun 11, 2025
6579aae
fix: @testing-library/react를 사용하도록 변경, include 파일 추가, 브라우저 모드 해제
hamo-o Jun 11, 2025
fa45124
test: 하루 종일이 아닌 일정 카드 리스트, 아이템 렌더링 테스트
hamo-o Jun 12, 2025
aa6e6fb
feat: 날짜 배열 정렬 함수 구현
hamo-o Jun 12, 2025
84b22e7
chore: date-time 패키지 테스트 설정
hamo-o Jun 12, 2025
50ebccb
test: 날짜 정렬 테스트
hamo-o Jun 12, 2025
359443e
refactor: 날짜 정렬 비즈니스 로직과 분리
hamo-o Jun 12, 2025
e7f78c9
feat: 겹치는 날짜 그룹핑 함수 구현
hamo-o Jun 12, 2025
b0b8097
fix: 불필요한 변수 삭제 및 내보내기
hamo-o Jun 12, 2025
86d1451
fix: 잘못된 포맷팅 로직 수정, 객체 비교를 time으로 할 수 있도록
hamo-o Jun 12, 2025
693a381
test: 겹치는 날짜 및 겹치지 않는 날짜 테스트
hamo-o Jun 12, 2025
95f9b46
refactor: 변수명 변경 및 jsDoc 추가
hamo-o Jun 12, 2025
0c2b548
feat: 다양한 객체 프로퍼티도 받을 수 있도록 확장
hamo-o Jun 12, 2025
e926049
refactor: DateRange 타입 분리
hamo-o Jun 12, 2025
eeb8a63
feat: 다양한 입력에 대응할 수 있도록 & getDateParts 메서드 추가
hamo-o Jun 12, 2025
03d3d6b
refactor: 날짜 그룹핑 비즈니스 로직과 분리
hamo-o Jun 12, 2025
e7fa4de
feat: Date, null 타입도 받을 수 있도록
hamo-o Jun 12, 2025
cb6dfd8
fix: TimeZone 포함한 문자열도 통과하도록
hamo-o Jun 12, 2025
c9cca19
chore: 테스트, 린트, 빌드 추가한 CI 스크립트
hamo-o Jun 12, 2025
45a2ecd
fix: 다음날로 넘어가는 날짜 생기지 않도록 테스트케이스 수정
hamo-o Jun 12, 2025
4a85d04
feat: 날짜 그룹핑 함수 구현
hamo-o Jun 12, 2025
ff44213
test: 날짜 그룹핑 테스트
hamo-o Jun 12, 2025
0cd3d83
refactor: 날짜 그룹핑 패키지 사용으로 전환
hamo-o Jun 12, 2025
b25a967
fix: 빌드 먼저 하도록 순서 변경
hamo-o Jun 12, 2025
2ac8ce1
fix: 잘못된 import 수정
hamo-o Jun 12, 2025
2f062f2
fix: 잘못된 alias 경로 수정
hamo-o Jun 12, 2025
a1b91d8
refactor: 중복된 상수 삭제
hamo-o Jun 12, 2025
40fea76
refactor: 스프레드 연산자 지양으로 시간복잡도 개선
hamo-o Jun 12, 2025
366c2be
chore: 테스트 주석처리 대신 skip 변경
hamo-o Jun 12, 2025
7de7e49
fix: null값이 들어오면 0이 아닌 NaN으로 원시값 설정
hamo-o Jun 12, 2025
4e412c0
test: EndolphinDate 깊은비교
hamo-o Jun 12, 2025
615811f
refactor: 중복된 타입 선언으로 교체
hamo-o Jun 13, 2025
3035966
fix: tsconfig에도 alias 추가하기
hamo-o Jun 13, 2025
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
53 changes: 53 additions & 0 deletions .github/workflows/fe-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: fe-ci

on:
pull_request:
branches:
- dev
paths:
- "frontend/**"

jobs:
frontend:
runs-on: ubuntu-latest

defaults:
run:
working-directory: ./frontend

steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Cache node_modules
uses: actions/cache@v4
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-

- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9

- name: Set .env file
run: |
echo "${{ secrets.FE_ENV }}" > .env

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Lint
run: pnpm lint

- name: Build Frontend
run: pnpm build

- name: Run Tests
run: pnpm test
19 changes: 19 additions & 0 deletions frontend/apps/client/__tests__/mocks/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { PersonalEventResponse } from '@/features/my-calendar/model';

export const createCards = (num: number): PersonalEventResponse[] =>
Array.from({ length: num }, (_, i) => {
const id = i + 1;
const baseDate = new Date('2023-10-01T00:00:00');
const start = new Date(baseDate.getTime() + i * 30 * 60 * 1000);
const end = new Date(start.getTime() + 60 * 60 * 1000);

return {
id,
title: `Test Event ${id}`,
startDateTime: start.toISOString(),
endDateTime: end.toISOString(),
isAdjustable: id % 2 === 1,
googleEventId: `google-event-id-${id}`,
calendarId: `calendar-id-${id}`,
};
});
43 changes: 43 additions & 0 deletions frontend/apps/client/__tests__/unit/my-calendar/card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { render } from '@testing-library/react';

import { CalendarCardList } from '@/features/my-calendar/ui/CalendarCardList';

import { createCards } from '../../mocks/events';

describe('CalendarCardList', () => {
it('일반 일정 카드 리스트 컨테이너 렌더링', () => {
// given
const cards = createCards(10);

// when
const { container } = render(<CalendarCardList cards={cards} isSelecting={true} />);
const cardList = container.querySelector('[class*="CalendarCardList"]');

// then
expect(cardList).toBeVisible();
});
it('일반 일정 카드 아이템 렌더링', () => {
// given
const cards = createCards(10);

// when
const { container } = render(<CalendarCardList cards={cards} isSelecting={true} />);

// then
const cardList = container.querySelector('[class*="CalendarCardList"]');
expect(cardList?.childElementCount).toBe(cards.length);
});
it.skip('겹치는 날짜 정렬 알고리즘 카드 렌더링 시간 100ms 이하', () => {
// given
const cards = createCards(1000);

// when
const start = performance.now();
render(<CalendarCardList cards={cards} isSelecting={true} />);
const end = performance.now();

// then
expect(end - start).toBeLessThan(100);
});
},
);
2 changes: 0 additions & 2 deletions frontend/apps/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@
"@tanstack/react-query": "^5.66.0",
"@tanstack/react-router": "^1.109.2",
"jotai": "^2.12.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"zod": "^3.24.1"
},
"devDependencies": {
Expand Down
1 change: 1 addition & 0 deletions frontend/apps/client/setup-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import '@testing-library/jest-dom';
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { calcPositionByDate } from '@endolphin/core/utils';
import type { EndolphinDate } from '@endolphin/date-time';

import { TIME_HEIGHT } from '@/constants/date';

import type { PersonalEventResponse } from '../../model';
import { CalendarCard } from '../CalendarCard';

const calcSize = (height: number) => {
if (height < TIME_HEIGHT) return 'sm';
if (height < TIME_HEIGHT * 2.5) return 'md';
return 'lg';
};

export const DefaultCard = (
{ card, start, end, idx }:
{ card: PersonalEventResponse; start: EndolphinDate; end: EndolphinDate; idx: number },
) => {
const LEFT_MARGIN = 24;
const RIGHT_MARGIN = 8;
const SIDEBAR_WIDTH = 72;
const TOP_GAP = 16;
const DAYS = 7;
const { x: sx, y: sy } = calcPositionByDate(start.getDate());
const { y: ey } = calcPositionByDate(end.getDate());

if (sy === ey) return null;

const height = ey - sy;
return (
<CalendarCard
calendarId={card.calendarId}
endTime={new Date(card.endDateTime)}
id={card.id}
size={calcSize(height)}
startTime={new Date(card.startDateTime)}
status={card.isAdjustable ? 'adjustable' : 'fixed'}
style={{
width: `calc(
(100% - ${SIDEBAR_WIDTH}px) / ${DAYS} - ${RIGHT_MARGIN}px - ${idx * LEFT_MARGIN}px)`,
height,
position: 'absolute',
left: `calc(((100% - ${SIDEBAR_WIDTH}px) / ${DAYS} * ${sx}) + ${SIDEBAR_WIDTH}px)`,
marginLeft: idx * LEFT_MARGIN,
top: TOP_GAP + sy,
}}
title={card.title}
/>
);
};

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { recipe } from '@vanilla-extract/recipes';

export const cardListStyle = recipe({
base: {},
variants: {
disable: {
true: {
pointerEvents: 'none',
},
false: {
pointerEvents: 'auto',
},
},
},
defaultVariants: {
disable: false,
},
});
Original file line number Diff line number Diff line change
@@ -1,78 +1,52 @@
import { TIME_HEIGHT } from '@constants/date';
import { calcPositionByDate, getDateParts, isAllday } from '@endolphin/core/utils';
import { isAllday } from '@endolphin/core/utils';
import type { GroupInfo } from '@endolphin/date-time';
import { EndolphinDate, groupByDate, groupByOverlap, sortDates } from '@endolphin/date-time';

import type { PersonalEventResponse } from '../../model';
import { CalendarCard } from '../CalendarCard';
import { DefaultCard } from './DefaultCard';
import { cardListStyle } from './index.css';

const calcSize = (height: number) => {
if (height < TIME_HEIGHT) return 'sm';
if (height < TIME_HEIGHT * 2.5) return 'md';
return 'lg';
const createGroupInfo = (card: PersonalEventResponse): GroupInfo<PersonalEventResponse> => {
const start = new EndolphinDate(card.startDateTime);
const end = new EndolphinDate(card.endDateTime);
const { year: sy, month: sm, day: sd } = start.getDateParts();
const { year: ey, month: em, day: ed } = end.getDateParts();
return {
id: card.id.toString(),
start,
end,
data: card,
sy, sm, sd,
ey, em, ed,
};
};

const DefaultCard = (
{ card, start, end }: { card: PersonalEventResponse; start: Date; end: Date },
) => {
const { x: sx, y: sy } = calcPositionByDate(start);
const { y: ey } = calcPositionByDate(end);

if (sy === ey) return null;
export const CalendarCardList = ({ cards, isSelecting }: {
cards: PersonalEventResponse[];
isSelecting: boolean;
}) => {
const isNotAlldayCards
= cards.filter((card)=>!isAllday(card.startDateTime, card.endDateTime)).map(createGroupInfo);

const height = ey - sy;
return (
<CalendarCard
calendarId={card.calendarId}
endTime={new Date(card.endDateTime)}
id={card.id}
size={calcSize(height)}
startTime={new Date(card.startDateTime)}
status={card.isAdjustable ? 'adjustable' : 'fixed'}
style={{
width: 'calc((100% - 72px) / 7 - 0.5rem)',
height,
position: 'absolute',
left: `calc(((100% - 72px) / 7 * ${sx}) + 72px)`,
top: 16 + sy,
}}
title={card.title}
/>
<div className={cardListStyle({ disable: isSelecting })}>
{groupByDate(isNotAlldayCards).map((dayCards) =>
groupByOverlap(dayCards.sort((a, b)=>
sortDates(
{ start: a.start, end: a.end },
{ start: b.start, end: b.end },
)))
.map(({ id, data, start, end, idx }) => (
<DefaultCard
card={data}
end={end}
idx={idx}
key={id}
start={start}
/>
),
),
)}
</div>
);
};

export const CalendarCardList = ({ cards }: { cards: PersonalEventResponse[] }) => (
<>
{cards.filter((card) => !isAllday(card.startDateTime, card.endDateTime))
.map((card) => {
const start = new Date(card.startDateTime);
const end = new Date(card.endDateTime);
const { year: sy, month: sm, day: sd } = getDateParts(start);
const { year: ey, month: em, day: ed } = getDateParts(end);

if (sd !== ed) {
return (
<div key={card.id}>
<DefaultCard
card={card}
end={new Date(sy, sm, sd, 23, 59)}
start={start}
/>
<DefaultCard
card={card}
end={end}
start={new Date(ey, em, ed, 0, 0)}
/>
</div>
);
}

return (
<DefaultCard
card={card}
end={end}
key={card.id}
start={start}
/>
);
})}
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const CalendarTable = (
type='add'
/>}
<CalendarDiscussionBox />
<CalendarCardList cards={personalEvents} />
<CalendarCardList cards={personalEvents} isSelecting={time.isSelecting} />
<CalendarTimeBar height={height} />
<Calendar.Table
context={{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { calcPositionByDate, formatDateToBarString, formatDateToTimeString } from '@endolphin/core/utils';
import {
calcPositionByDate,
formatDateToBarString,
formatDateToTimeString,
} from '@endolphin/core/utils';
import { Flex } from '@endolphin/ui';
import { useFormRef } from '@hooks/useFormRef';

Expand Down
7 changes: 4 additions & 3 deletions frontend/apps/client/tsconfig.app.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": ".",
"types": ["vitest/globals"],
"types": ["vitest/globals", "@vitest/browser/providers/playwright"],
"paths": {
"@/*": ["src/*"],
"@hooks/*": ["src/hooks/*"],
"@utils/*": ["src/utils/*"],
"@constants/*": ["src/constants/*"],
"@components/*": ["src/components/*"]
"@components/*": ["src/components/*"],
"@features/*": ["src/features/*"],
},
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"allowImportingTsExtensions": true,
"noEmit": true,
},
"include": ["src"]
"include": ["src", "__tests__"]
}
1 change: 1 addition & 0 deletions frontend/apps/client/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export default defineConfig({
'@utils': path.resolve(dirname, 'src/utils'),
'@constants': path.resolve(dirname, 'src/constants'),
'@components': path.resolve(dirname, 'src/components'),
'@features': path.resolve(dirname, 'src/features'),
},
},
ssr: {
Expand Down
9 changes: 8 additions & 1 deletion frontend/apps/client/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,15 @@ export default mergeConfig(
reactConfig,
defineProject({
root: dirname,
test: {
include: ['__tests__/unit/**/*.ts?(x)'],
setupFiles: ['./setup-file.ts'],
},
resolve: {
alias: createAlias(dirname),
alias: {
...createAlias(dirname),
'@features': path.resolve(dirname, 'src/features'),
},
},
}),
);
Loading