-
Notifications
You must be signed in to change notification settings - Fork 1
[FE-Feat] 겹치는 일정 처리하기 #329
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
54 commits
Select commit
Hold shift + click to select a range
c2e279f
feat: 날짜 겹침 로직
hamo-o 8b2ed35
feat: 겹치는 시간대끼리 그룹핑
hamo-o 8ea417c
refactor: 매직넘버 삭제
hamo-o 1b186ae
feat: 현재 캘린더 드래그 중인지를 나타내는 isSelecting 필드 훅에 추가
hamo-o db0d5fc
fix: isSelecting일때는 카드 이벤트를 막아서 드래그 이벤트 버블링이 정상적으로 동작하도록 하기
hamo-o ac97ab8
fix: defaultVariants 추가
hamo-o ecd9110
chore: @vitest/browser, playwright 설치 및 vanilla extract 업데이트
hamo-o 8b238c2
chore: vitest-browser-react 설치
hamo-o 1423c1f
chore: 불필요한 테스팅 라이브러리 삭제
hamo-o a3b5ea2
chore: 정상적 모듈 인식을 위해 @vanilla-extract/vite-plugin 다운그레이드
hamo-o 67672c4
chore: vitest browser를 위한 세팅
hamo-o 4ea5b88
chore: vitest browser config 패키지 분리
hamo-o fcf9ecb
chore: vitest가 VE를 이해할 수 있도록 플러그인 세팅 추가
hamo-o 478a22d
chore: 타입 추론을 위한 세팅 파일 추가
hamo-o 21f8ff4
fix: react 문법 이해를 위한 플러그인 추가
hamo-o 70e6f36
chore: 브라우저 크기 데스크탑으로 변경
hamo-o f3788c6
fix: 테스트 범위는 소비자 쪽에서 작성하도록 수정
hamo-o 6d7495a
test: 캘린더 리스트 렌더링 시간
hamo-o f5bf185
chore: vitest-browser-react 대신 @testing-library 재설치
hamo-o 55adc62
chore: react 루트로 의존성 이동, @testing-library/jest-dom 설치
hamo-o 00088fd
fix: 빠진 react 플러그인 추가
hamo-o 52e5495
chore: @testing-library/jest-dom setup
hamo-o 6579aae
fix: @testing-library/react를 사용하도록 변경, include 파일 추가, 브라우저 모드 해제
hamo-o fa45124
test: 하루 종일이 아닌 일정 카드 리스트, 아이템 렌더링 테스트
hamo-o aa6e6fb
feat: 날짜 배열 정렬 함수 구현
hamo-o 84b22e7
chore: date-time 패키지 테스트 설정
hamo-o 50ebccb
test: 날짜 정렬 테스트
hamo-o 359443e
refactor: 날짜 정렬 비즈니스 로직과 분리
hamo-o e7f78c9
feat: 겹치는 날짜 그룹핑 함수 구현
hamo-o b0b8097
fix: 불필요한 변수 삭제 및 내보내기
hamo-o 86d1451
fix: 잘못된 포맷팅 로직 수정, 객체 비교를 time으로 할 수 있도록
hamo-o 693a381
test: 겹치는 날짜 및 겹치지 않는 날짜 테스트
hamo-o 95f9b46
refactor: 변수명 변경 및 jsDoc 추가
hamo-o 0c2b548
feat: 다양한 객체 프로퍼티도 받을 수 있도록 확장
hamo-o e926049
refactor: DateRange 타입 분리
hamo-o eeb8a63
feat: 다양한 입력에 대응할 수 있도록 & getDateParts 메서드 추가
hamo-o 03d3d6b
refactor: 날짜 그룹핑 비즈니스 로직과 분리
hamo-o e7fa4de
feat: Date, null 타입도 받을 수 있도록
hamo-o cb6dfd8
fix: TimeZone 포함한 문자열도 통과하도록
hamo-o c9cca19
chore: 테스트, 린트, 빌드 추가한 CI 스크립트
hamo-o 45a2ecd
fix: 다음날로 넘어가는 날짜 생기지 않도록 테스트케이스 수정
hamo-o 4a85d04
feat: 날짜 그룹핑 함수 구현
hamo-o ff44213
test: 날짜 그룹핑 테스트
hamo-o 0cd3d83
refactor: 날짜 그룹핑 패키지 사용으로 전환
hamo-o b25a967
fix: 빌드 먼저 하도록 순서 변경
hamo-o 2ac8ce1
fix: 잘못된 import 수정
hamo-o 2f062f2
fix: 잘못된 alias 경로 수정
hamo-o a1b91d8
refactor: 중복된 상수 삭제
hamo-o 40fea76
refactor: 스프레드 연산자 지양으로 시간복잡도 개선
hamo-o 366c2be
chore: 테스트 주석처리 대신 skip 변경
hamo-o 7de7e49
fix: null값이 들어오면 0이 아닌 NaN으로 원시값 설정
hamo-o 4e412c0
test: EndolphinDate 깊은비교
hamo-o 615811f
refactor: 중복된 타입 선언으로 교체
hamo-o 3035966
fix: tsconfig에도 alias 추가하기
hamo-o File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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}`, | ||
| }; | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"]'); | ||
|
|
||
hamo-o marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // 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); | ||
| }); | ||
| }, | ||
| ); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| import '@testing-library/jest-dom'; |
51 changes: 51 additions & 0 deletions
51
frontend/apps/client/src/features/my-calendar/ui/CalendarCardList/DefaultCard.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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} | ||
| /> | ||
| ); | ||
| }; | ||
|
|
18 changes: 18 additions & 0 deletions
18
frontend/apps/client/src/features/my-calendar/ui/CalendarCardList/index.css.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | ||
| }, | ||
| }); |
114 changes: 44 additions & 70 deletions
114
frontend/apps/client/src/features/my-calendar/ui/CalendarCardList/index.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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} | ||
| /> | ||
| ); | ||
| })} | ||
| </> | ||
| ); | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
6 changes: 5 additions & 1 deletion
6
frontend/apps/client/src/features/my-calendar/ui/SchedulePopover/index.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.