Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
7d8dc1e
feat: 애플 로그인 버튼 추가
sungtaegyeong Jul 31, 2026
1d4d6e9
refactor: 소셜 약관 동의 플래그 공통화
sungtaegyeong Jul 31, 2026
f52b2cb
feat: 애플 로그인 콜백 처리 추가
sungtaegyeong Jul 31, 2026
d156cea
Merge pull request #313 from check-task/feat/#312
sungtaegyeong Jul 31, 2026
3dc9302
feat: DatePicker 날짜 미지정 옵션 추가
sungtaegyeong Aug 4, 2026
c99a3be
feat: 세부 TASK 날짜 미지정 연동
sungtaegyeong Aug 4, 2026
0f01c5b
fix: 날짜 미지정 표시 문구 변경
sungtaegyeong Aug 4, 2026
b5bca11
fix: 세부 TASK 날짜 미지정 선택 상태 반영
sungtaegyeong Aug 4, 2026
30dd152
fix: 날짜 미지정 체크 상태 동작 수정
sungtaegyeong Aug 4, 2026
0bde4ae
style: 날짜 미지정 영역 간격 조정
sungtaegyeong Aug 4, 2026
3b1b1d1
fix: 날짜 미지정 초기 선택 상태 정리
sungtaegyeong Aug 4, 2026
a32abd9
fix: 날짜 미지정 시 알림 토글 상태 초기화
sungtaegyeong Aug 4, 2026
84dc671
Merge pull request #315 from check-task/feat/#314
sungtaegyeong Aug 4, 2026
542b054
fix: 홈 세부 TASK 날짜 미지정 값 보존
sungtaegyeong Aug 5, 2026
1cacc5f
feat: 날짜 미지정 TASK 영역 추가
sungtaegyeong Aug 5, 2026
7f92c88
feat: 미지정 TASK 캘린더 드래그 연동
sungtaegyeong Aug 6, 2026
cea7c51
fix: 캘린더 세부 TASK 캐시 갱신 안정화
sungtaegyeong Aug 6, 2026
baf5a79
fix: 상위 과제 마감일 미지정 시 세부 TASK 드롭 허용
sungtaegyeong Aug 6, 2026
fe01b50
fix: 미지정 TASK 펼치기 버튼 접근성 속성 추가
sungtaegyeong Aug 6, 2026
67760ee
fix: 세부 TASK 마감일 변경 실패 시 캐시 복구
sungtaegyeong Aug 6, 2026
f936d96
Merge pull request #317 from check-task/feat/#316
sungtaegyeong Aug 6, 2026
a902c6f
feat: 3차 릴리즈 공지사항 추가
sungtaegyeong Aug 7, 2026
215f224
Merge pull request #319 from check-task/feat/#318
sungtaegyeong Aug 7, 2026
6affc58
feat: 팀원 프로필 보기 내용 컴포넌트 추가 및 화면 연결
yewon20804 Aug 9, 2026
ddef03a
feat: 담당자 선택 드롭다운 여러명 선택 가능하도록 UI 수정
yewon20804 Aug 9, 2026
96c12c2
feat: 담당자 여러 명 표시용 그룹 칩 추가 및 기존 제거
yewon20804 Aug 9, 2026
f68adcd
fix: 팀원 프로필 빈 문자열도 '-'로 표시
yewon20804 Aug 9, 2026
310804c
fix: 담당자 칩 그룹의 button 내부 div를 span으로 변경
yewon20804 Aug 9, 2026
7fbef84
fix: 담당자 선택 드롭다운 접근성 개선 (aria-multiselectable 추가)
yewon20804 Aug 9, 2026
301a5e5
Merge pull request #321 from check-task/feat/#320
yewon20804 Aug 9, 2026
3c06e2e
feat: 팀과제 특정 팀원 프로필 조회 API 추가
yewon20804 Aug 9, 2026
b4afbad
feat: 팀원 프로필 모달에서 API 조회 데이터 표시
yewon20804 Aug 9, 2026
73e84a3
fix: 세부과제 생성 실패 시 캐시 롤백 추가
yewon20804 Aug 9, 2026
d4aa886
feat: 마이페이지 폴더 클릭 시 해당 폴더로 필터링된 개인 과제 목록으로 이동
yewon20804 Aug 9, 2026
4213313
fix: 팀원 프로필 모달 너비 고정 해제 및 이메일 라벨 줄바꿈 방지
yewon20804 Aug 9, 2026
88b2b13
style: 팀원 관리 목록 프로필 이니셜 텍스트 제거
yewon20804 Aug 9, 2026
3d5a74b
refactor: 팀원 프로필 연락처 미설정 안내 문구를 빈 값으로 처리
yewon20804 Aug 9, 2026
2fa5718
refactor: 세부 TASK 담당자 다인원으로 타입 및 응답 매핑 변경
yewon20804 Aug 11, 2026
4f04aa3
refactor: 세부 담장자 설정 REST/소켓 다인원 전송으로 전환
yewon20804 Aug 11, 2026
bddec21
feat: 팀 과제 페이지에 담당자 다인원 지정 API 연결
yewon20804 Aug 11, 2026
ae9cd63
fix: 팀원 프로필 모달 진입 시 깜빡임 방지를 위한 프리페치 추가
yewon20804 Aug 11, 2026
86448c8
fix: 폴더 필터 folderId 검증 및 URL 변경 시 상태 동기화 추가
yewon20804 Aug 11, 2026
56ecaec
fix: 담당자 드롭다운 대상 목록 id 기준을 memberId에서 userId로 변경
yewon20804 Aug 11, 2026
7388d4c
fix: 세부과제 동시 생성 시 실패한 항목만 롤백하도록 개선
yewon20804 Aug 11, 2026
42caeb2
Merge pull request #323 from check-task/feat/#332
yewon20804 Aug 11, 2026
1a40be6
chore: 버전 3.0.0 업데이트
sungtaegyeong Aug 11, 2026
6259df8
fix: 3차 릴리즈 공지 날짜 수정
sungtaegyeong Aug 18, 2026
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "check-task",
"version": "2.0.0",
"version": "3.0.0",
"private": true,
"scripts": {
"prepare": "panda codegen",
Expand Down
Binary file added public/apple-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 27 additions & 3 deletions src/app/(main)/assignment/page.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,33 @@
'use client';

import { useState } from 'react';
import { Suspense, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { css } from 'styled-system/css';
import { AssignmentList } from '@/components/AssignmentList';
import { FolderFilterDropdown } from '@/components/FolderFilterDropdown';
import { useTaskList } from '@/features/assignment/hooks/useTaskList';
import { useMyInfo } from '@/hooks/queries/useMyInfo';

export default function AssignmentPage() {
function AssignmentPageContent() {
const searchParams = useSearchParams();
const folderIdParam = searchParams.get('folderId');
const parsedFolderId = folderIdParam ? Number(folderIdParam) : NaN;
const initialFolderId = Number.isSafeInteger(parsedFolderId)
? parsedFolderId
: null;

const { data: myInfo } = useMyInfo();
const folders = myInfo?.folders ?? [];
// 마이페이지에서 폴더 클릭 시 넘어온 folderId로 초기 필터 지정
const [prevInitialFolderId, setPrevInitialFolderId] = useState(initialFolderId);
const [selectedFolderIds, setSelectedFolderIds] = useState<number[] | null>(
null,
() => (initialFolderId != null ? [initialFolderId] : null),
);
// 같은 페이지에서 URL의 folderId만 바뀌는 경우 대비, 렌더 중 동기화
if (initialFolderId !== prevInitialFolderId) {
setPrevInitialFolderId(initialFolderId);
setSelectedFolderIds(initialFolderId != null ? [initialFolderId] : null);
}
// 과제 목록 데이터 가져오기
const { data = [], isLoading } = useTaskList(selectedFolderIds);

Expand All @@ -27,6 +42,7 @@ export default function AssignmentPage() {
<h3 className={titleStyle}>내 과제</h3>
<FolderFilterDropdown
folders={folders}
initialFolderId={initialFolderId}
onSelectionChange={setSelectedFolderIds}
/>
</div>
Expand All @@ -39,6 +55,14 @@ export default function AssignmentPage() {
);
}

export default function AssignmentPage() {
return (
<Suspense>
<AssignmentPageContent />
</Suspense>
);
}

// ======== 스타일 정의 ========
const headerStyle = css({
display: 'flex',
Expand Down
2 changes: 1 addition & 1 deletion src/app/(main)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default function Home() {

const pageStyle = css(
stack.raw({
gap: '1.5rem',
gap: '1.25rem',
marginX: 'auto',
marginTop: '2rem',
marginBottom: '3.75rem',
Expand Down
58 changes: 58 additions & 0 deletions src/app/auth/apple/callback/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
'use client';

import { useCallback, useEffect, useRef } from 'react';
import { useRouter } from 'next/navigation';
import { css } from 'styled-system/css';
import axiosInstance from '@/lib/axiosInstance';
import { SOCIAL_AGREEMENT_REQUIRED_KEY } from '@/features/login/constants/socialAgreement';
import { useAuthStore } from '@/stores/auth-store';

export default function AppleAuthCallbackPage() {
return <CallbackHandler />;
}

function CallbackHandler() {
const router = useRouter();
const login = useAuthStore((state) => state.login);
const isProcessing = useRef(false);

const getAccessToken = useCallback(async () => {
try {
const response = await axiosInstance.post('/auth/refresh');
const { accessToken } = response.data.data;

if (accessToken) {
login(accessToken);

const params = new URLSearchParams(window.location.search);
if (params.get('isNewUser') === 'true') {
window.sessionStorage.setItem(SOCIAL_AGREEMENT_REQUIRED_KEY, 'true');
}

router.replace('/');
return;
}

console.error('accessToken이 응답에 없습니다');
router.replace('/login');
} catch (error) {
console.error('Apple 로그인 토큰 발급 실패:', error);
router.replace('/login');
}
}, [router, login]);

useEffect(() => {
if (isProcessing.current) return;
isProcessing.current = true;
getAccessToken();
}, [getAccessToken]);

return <div className={loadingStyle}>로그인 처리 중...</div>;
}

const loadingStyle = css({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
});
4 changes: 2 additions & 2 deletions src/app/auth/kakao/callback/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useRouter } from 'next/navigation';
import { css } from 'styled-system/css';
import axiosInstance from '@/lib/axiosInstance';
import { useAuthStore } from '@/stores/auth-store';
import { KAKAO_AGREEMENT_REQUIRED_KEY } from '@/features/login/constants/kakaoAgreement';
import { SOCIAL_AGREEMENT_REQUIRED_KEY } from '@/features/login/constants/socialAgreement';

export default function AuthCallbackPage() {
return <CallbackHandler />;
Expand All @@ -28,7 +28,7 @@ function CallbackHandler() {
login(accessToken); // 토큰 저장
const params = new URLSearchParams(window.location.search);
if (params.get('isNewUser') === 'true') {
window.sessionStorage.setItem(KAKAO_AGREEMENT_REQUIRED_KEY, 'true');
window.sessionStorage.setItem(SOCIAL_AGREEMENT_REQUIRED_KEY, 'true');
}
router.replace('/');
} else {
Expand Down
92 changes: 80 additions & 12 deletions src/components/DatePicker/CalendarModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,30 @@

import Calendar from 'react-calendar';
import 'react-calendar/dist/Calendar.css';
import { css } from 'styled-system/css';
import { css, cva } from 'styled-system/css';
import { flex } from 'styled-system/patterns';
import { RightIcon } from '../icons/RightIcon';
import { LeftIcon } from '../icons/LeftIcon';
import { useState } from 'react';
import { Button } from '../Button';
import { TimeToggle } from '../TimeToggle';
import { useAlertStore } from '@/stores/alert-store';
import { CheckCircleIcon } from '../icons/CheckCircleIcon';
import { token } from 'styled-system/tokens';

interface CalenderModalProps {
onClose: () => void;
onSave: (date: Date, timeEnabled: boolean) => void;
onSave: (date: Date | null, timeEnabled: boolean) => void;
// 초기 날짜를 받기 위함
initialDate: Date;
initialDate: Date | null;
// 세부 목록 날짜 선택시 이후 날짜 제한을 위해 추가
maxDate?: Date;
// 시간 추가 토글 초기값
initialTimeEnabled?: boolean;
// 시간 추가 토글 상태 변경 콜백
onTimeToggle?: (enabled: boolean) => void;
// 날짜 지정안함 옵션 노출 여부
allowUnspecified?: boolean;
}

export default function CalendarModal({
Expand All @@ -31,19 +35,25 @@ export default function CalendarModal({
maxDate,
initialTimeEnabled = false,
onTimeToggle,
allowUnspecified = false,
}: CalenderModalProps) {
// 선택된 날짜
const { showAlert } = useAlertStore();
const [selectedDate, setSelectedDate] = useState<Date | null>(initialDate);
const [isUnspecifiedSelected, setIsUnspecifiedSelected] = useState(
allowUnspecified && !initialDate,
);
// 시간 추가 토글 상태
const [timeEnabled, setTimeEnabled] = useState(initialTimeEnabled);
const [timeEnabled, setTimeEnabled] = useState(
initialDate ? initialTimeEnabled : false,
);
// 시간 입력 상태 — 모달을 다시 열 때 저장된 시간 복원
const [period, setPeriod] = useState<'오전' | '오후'>(() => {
if (!initialTimeEnabled) return '오전';
if (!initialDate || !initialTimeEnabled) return '오전';
return initialDate.getHours() < 12 ? '오전' : '오후';
});
const [timeValue, setTimeValue] = useState(() => {
if (!initialTimeEnabled) return '00:00';
if (!initialDate || !initialTimeEnabled) return '00:00';
const h = initialDate.getHours() % 12 || 12;
const m = initialDate.getMinutes();
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
Expand Down Expand Up @@ -102,6 +112,7 @@ export default function CalendarModal({
};

const handleTimeToggle = (enabled: boolean) => {
if (!selectedDate) return;
setTimeEnabled(enabled);
onTimeToggle?.(enabled);
};
Expand All @@ -113,9 +124,17 @@ export default function CalendarModal({
// 배열 말고 단일 선택만 고려
if (value instanceof Date || value === null) {
setSelectedDate(value);
setIsUnspecifiedSelected(false);
}
};

const handleSelectUnspecified = () => {
setSelectedDate(null);
setIsUnspecifiedSelected(true);
setTimeEnabled(false);
onTimeToggle?.(false);
};

return (
<div className={modalWrapper}>
<Calendar
Expand All @@ -140,9 +159,32 @@ export default function CalendarModal({
showFixedNumberOfWeeks={true} // 6주를 보여주는 프롭이 있음
/>
<div className={timeSection}>
{allowUnspecified && (
<button
type='button'
className={unspecifiedDateRow}
onClick={handleSelectUnspecified}
>
<span className={timeToggleLabel()}>날짜 지정안함</span>
<CheckCircleIcon
size={24}
color={
isUnspecifiedSelected
? token('colors.gray.900')
: token('colors.gray.300')
}
/>
</button>
)}
<div className={timeToggleRow}>
<span className={timeToggleLabel}>시간 추가</span>
<TimeToggle checked={timeEnabled} onChange={handleTimeToggle} />
<span className={timeToggleLabel({ disabled: !selectedDate })}>
시간 추가
</span>
<TimeToggle
checked={timeEnabled}
onChange={handleTimeToggle}
disabled={!selectedDate}
/>
</div>
{timeEnabled && selectedDate && (
<div className={dateTimeRow}>
Expand Down Expand Up @@ -192,6 +234,13 @@ export default function CalendarModal({
variant='fillBlue'
size='small'
onClick={() => {
if (!selectedDate) {
if (allowUnspecified) {
onSave(null, false);
}
return;
}

if (selectedDate) {
if (timeEnabled) {
const [hStr, mStr] = timeValue.split(':');
Expand Down Expand Up @@ -406,19 +455,38 @@ const modalWrapper = css({
const timeSection = css({
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
gap: '0.75rem',
mt: '0.75rem',
});

const unspecifiedDateRow = flex({
alignItems: 'center',
justifyContent: 'space-between',
bg: 'transparent',
border: 'none',
p: '0',
cursor: 'pointer',
});

// 시간 추가 토글 행
const timeToggleRow = flex({
alignItems: 'center',
justifyContent: 'space-between',
});

const timeToggleLabel = css({
textStyle: 'body3.m',
color: 'gray.900',
const timeToggleLabel = cva({
base: {
textStyle: 'body3.m',
color: 'gray.900',
},
variants: {
disabled: {
true: { color: 'gray.400' },
},
},
defaultVariants: {
disabled: false,
},
});

// 날짜 + 시간 입력 행
Expand Down
Loading