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
8 changes: 7 additions & 1 deletion src/components/features/card-list/card-list-filter-chips.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ export function CardListFilterChips({
>
<Icon name="chevronUp" size={16} color={colors.gray[700]} />
</Pressable>
<View>
<View style={styles.dropdownOptions}>
<View>
{expanded.key === 'personal' && isPersonalTagsLoading ? (
<Typography variant="bodyS" color={colors.gray[400]}>
Expand Down Expand Up @@ -368,6 +368,7 @@ const styles = StyleSheet.create({
zIndex: 20,
flexDirection: 'row',
alignItems: 'flex-start',
maxWidth: '100%',
gap: spacing[2] + 2,
paddingVertical: spacing[2] - 2,
paddingLeft: spacing[1] + 1,
Expand All @@ -380,6 +381,11 @@ const styles = StyleSheet.create({
},
selectedOptionsRow: {
flexDirection: 'row',
flexShrink: 1,
flexWrap: 'wrap',
},
dropdownOptions: {
flexShrink: 1,
},
emptyText: {
textAlign: 'center',
Expand Down
69 changes: 38 additions & 31 deletions src/components/features/condition/condition-quadrant-plot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ export function ConditionQuadrantPlot({
showOrigin = true,
}: ConditionQuadrantPlotProps) {
const interaction = useConditionQuadrantInteraction(onSelect);
const activeHistoryPoint = points.find(
(point) => point.id === activeMarkerId && (point.count ?? 1) >= 2,
);

return (
<View style={styles.container} onLayout={interaction.onLayout}>
Expand Down Expand Up @@ -242,37 +245,6 @@ export function ConditionQuadrantPlot({
{activeMarkerTime}
</Typography>
) : null}
{active && badge && activeMarkerRecords.length > 0 ? (
<View
style={[styles.historyList, toConditionHistoryListPosition(point.x, point.y)]}
>
{activeMarkerRecords.map((record, index) => (
<View key={record.id}>
{index > 0 ? <View style={styles.historyDivider} /> : null}
<Pressable
accessibilityLabel={`${record.label} 기록 선택`}
accessibilityRole="button"
accessibilityState={{ selected: selectedHistoryRecordId === record.id }}
hitSlop={spacing[1]}
style={[
styles.historyRecord,
selectedHistoryRecordId === record.id && styles.historyRecordSelected,
]}
onPress={() => onHistoryRecordPress?.(record.id)}
>
<Typography
variant="caption"
align="center"
color={colors.gray[700]}
numberOfLines={1}
>
{record.label}
</Typography>
</Pressable>
</View>
))}
</View>
) : null}
</View>
);
}
Expand All @@ -292,6 +264,41 @@ export function ConditionQuadrantPlot({
);
})}

{activeHistoryPoint != null && activeMarkerRecords.length > 0 ? (
<View
style={[
styles.historyList,
toConditionHistoryListPosition(activeHistoryPoint.x, activeHistoryPoint.y),
]}
>
{activeMarkerRecords.map((record, index) => (
<View key={record.id}>
{index > 0 ? <View style={styles.historyDivider} /> : null}
<Pressable
accessibilityLabel={`${record.label} 기록 선택`}
accessibilityRole="button"
accessibilityState={{ selected: selectedHistoryRecordId === record.id }}
hitSlop={spacing[1]}
style={[
styles.historyRecord,
selectedHistoryRecordId === record.id && styles.historyRecordSelected,
]}
onPress={() => onHistoryRecordPress?.(record.id)}
>
<Typography
variant="caption"
align="center"
color={colors.gray[700]}
numberOfLines={1}
>
{record.label}
</Typography>
</Pressable>
</View>
))}
</View>
) : null}

{value != null ? (
<View
style={[styles.valueDot, toConditionQuadrantPosition(value.x, value.y)]}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export function ConvertToPinBottomSheet({
setCandidateIndex(0);
setKeepOriginal(defaultKeepOriginal);
setDurationMinutes(Math.max(0, originalDurationMinutes - 10));
setMode('loading');
}, [defaultKeepOriginal, originalDurationMinutes, visible]);

useEffect(() => {
Expand Down
11 changes: 8 additions & 3 deletions src/components/features/sleep/sleep-week-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,16 @@ interface SleepWeekPickerProps {
monthLabel: string;
days: SleepWeekDay[];
onSelect: (id: string) => void;
onMoveWeek: (direction: 'previous' | 'next') => void;
onShiftDateRange: (direction: 'previous' | 'next') => void;
}

/** 수면 측정 카드의 주간 날짜 범위 선택기입니다. (취침일~기상일) */
export function SleepWeekPicker({ monthLabel, days, onSelect, onMoveWeek }: SleepWeekPickerProps) {
export function SleepWeekPicker({
monthLabel,
days,
onSelect,
onShiftDateRange,
}: SleepWeekPickerProps) {
const translateX = useSharedValue(0);
const hasMovedDate = useSharedValue(false);
const slideStyle = useAnimatedStyle(() => ({
Expand All @@ -50,7 +55,7 @@ export function SleepWeekPicker({ monthLabel, days, onSelect, onMoveWeek }: Slee
if (hasMovedDate.value || Math.abs(event.translationX) < DATE_CHANGE_THRESHOLD) return;

hasMovedDate.value = true;
runOnJS(onMoveWeek)(event.translationX < 0 ? 'previous' : 'next');
runOnJS(onShiftDateRange)(event.translationX < 0 ? 'previous' : 'next');
})
.onEnd(() => {
translateX.value = withSpring(0, { damping: 18, stiffness: 260 });
Expand Down
2 changes: 1 addition & 1 deletion src/domains/ai-recommendation/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
import {
getEmptyTimeRecommendSetting as getRecommendationCriteriaSetting,
updateEmptyTimeRecommendSetting as updateRecommendationCriteriaSetting,
} from '@/lib/api/endpoints/setting-controller/setting-controller';
} from '@/lib/api/endpoints/setting/setting';

import {
toConditionRecommendationViewModel,
Expand Down
8 changes: 8 additions & 0 deletions src/domains/ai-recommendation/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@ export function isUpcomingScheduleRecommendation(
return startMinutes > now.getHours() * 60 + now.getMinutes();
}

/** 마감일이 있는 큐 카드는 마감일 이후의 추천 슬롯을 노출하지 않습니다. */
export function isScheduleRecommendationWithinDeadline(
recommendation: Pick<ScheduleRecommendation, 'date'>,
dueDate: string,
): boolean {
return dueDate.length === 0 || recommendation.date <= dueDate;
}

export type QueueTimeRecommendationErrorMode = 'error-no-duration' | 'error-7day' | 'error-14day';

export type RecommendationAcceptErrorKind = 'expired' | 'conflict' | 'network' | 'unknown';
Expand Down
8 changes: 1 addition & 7 deletions src/domains/auth/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,7 @@
* 소셜 로그인, 토큰 재발급, 로그아웃, 탈퇴 호출을 감싸고
* token storage와 화면이 사용할 세션 모델로 변환합니다.
*/
import {
googleLogin,
kakaoLogin,
logout,
reissue,
withdraw,
} from '@/lib/api/endpoints/auth-controller/auth-controller';
import { googleLogin, kakaoLogin, logout, reissue, withdraw } from '@/lib/api/endpoints/auth/auth';
import { tokenStorage } from '@/lib/auth/token-storage';

import { toAuthSession, toReissuedAuthSession } from './mapper';
Expand Down
18 changes: 14 additions & 4 deletions src/domains/condition/quadrant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,23 @@ export function snapConditionQuadrantValue(value: number): number {
}

export function toConditionHistoryListPosition(x: number, y: number) {
const position = toConditionQuadrantPosition(x, y);
const left = Number.parseFloat(position.left);
const top = Number.parseFloat(position.top);

return {
...(x <= 0
? { left: CONDITION_QUADRANT.markerSize / 2 }
: { right: CONDITION_QUADRANT.markerSize / 2 }),
? { left: position.left, marginLeft: CONDITION_QUADRANT.markerSize / 2 }
: {
right: `${100 - left}%` as `${number}%`,
marginRight: CONDITION_QUADRANT.markerSize / 2,
}),
...(y >= 0
? { top: CONDITION_QUADRANT.markerSize / 2 }
: { bottom: CONDITION_QUADRANT.markerSize / 2 }),
? { top: position.top, marginTop: CONDITION_QUADRANT.markerSize / 2 }
: {
bottom: `${100 - top}%` as `${number}%`,
marginBottom: CONDITION_QUADRANT.markerSize / 2,
}),
};
}

Expand Down
7 changes: 2 additions & 5 deletions src/domains/member/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,8 @@
* member 도메인의 서버 API 경계입니다.
* 프로필과 알림 설정 조회/수정을 감싸 settings/home 화면이 generated DTO에 의존하지 않게 합니다.
*/
import { getProfile, updateProfile } from '@/lib/api/endpoints/member-controller/member-controller';
import {
getAlarmSetting,
updateAlarmSetting,
} from '@/lib/api/endpoints/setting-controller/setting-controller';
import { getProfile, updateProfile } from '@/lib/api/endpoints/member/member';
import { getAlarmSetting, updateAlarmSetting } from '@/lib/api/endpoints/setting/setting';

import {
toAlarmSettingRequest,
Expand Down
9 changes: 7 additions & 2 deletions src/domains/schedule/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
searchSchedules as searchScheduleEndpoints,
updateSchedule,
} from '@/lib/api/endpoints/schedule-crud/schedule-crud';
import { recommendTag } from '@/lib/api/endpoints/tag-controller/tag-controller';
import { recommendTag } from '@/lib/api/endpoints/tag/tag';

import {
normalizeDateForRequest,
Expand Down Expand Up @@ -159,7 +159,12 @@ export async function submitScheduleUpdate(
scheduleId: number,
input: ScheduleUpdateInput,
): Promise<ScheduleDetail> {
const response = await updateSchedule(scheduleId, toScheduleUpdateRequest(input));
const request = toScheduleUpdateRequest(input);
// OpenAPI의 nullable 누락으로 생성 타입에는 null이 없지만, 서버는 null로 기간 일정을 단일 일정으로 바꿉니다.
const response = await updateSchedule(
scheduleId,
request as Parameters<typeof updateSchedule>[1],
);

return toScheduleDetail(response);
}
Expand Down
31 changes: 24 additions & 7 deletions src/domains/schedule/api/mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ import type {

const API_WEEKDAY_CODES = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'] as const;

/**
* OpenAPI 명세는 `end_date`의 명시적 null을 설명하지만 nullable로 선언하지 않아
* Orval 생성 타입에는 null이 빠져 있습니다. API 경계에서만 실제 요청 형태를 보완합니다.
*/
export type ScheduleUpdateRequestWithNullableEndDate = Omit<ScheduleUpdateRequest, 'end_date'> & {
end_date?: string | null;
};

/** 서버 개인 태그를 화면에서 쓰는 태그 option으로 변환합니다. */
export function toPersonalTagOptions(response?: PersonalTagResponse[]) {
return (response ?? []).flatMap((tag) => {
Expand Down Expand Up @@ -135,6 +143,7 @@ export function toScheduleListItem(response: ScheduleGetResponse): ScheduleListI
id: response.schedule_id ?? 0,
title: response.title ?? '',
date: normalizeDateForView(response.date),
endDate: normalizeDateForView(response.end_date),
startTime: normalizeTimeToMinute(response.start_time ?? ''),
endTime: normalizeTimeToMinute(response.end_time ?? ''),
estimatedMinutes: response.estimated_time ?? null,
Expand Down Expand Up @@ -173,6 +182,7 @@ function toScheduleSearchListItem(response: ScheduleSearchResponse): ScheduleLis
id: response.schedule_id ?? 0,
title: response.title ?? '',
date: normalizeDateForView(response.date),
endDate: normalizeDateForView(response.end_date),
startTime: normalizeTimeToMinute(response.start_time ?? ''),
endTime: normalizeTimeToMinute(response.end_time ?? ''),
estimatedMinutes: response.estimated_time ?? null,
Expand Down Expand Up @@ -203,22 +213,22 @@ export function toScheduleSearchParams(input: {
personalTags: toOptionalArray(
input.personalTags?.map((tag) => tag.trim()).filter((tag) => tag.length > 0),
),
startDate: toSearchDateTime(input.startDate, input.startTime),
endDate: toSearchDateTime(input.endDate, input.endTime),
startDate: toSearchDateTime(input.startDate, input.startTime, '00:00'),
endDate: toSearchDateTime(input.endDate, input.endTime, '23:59'),
page: input.page,
};
}

function toSearchDateTime(date?: string, time?: string) {
function toSearchDateTime(date?: string, time?: string, defaultTime = '00:00') {
const normalizedDate = normalizeDateForRequest(date);

if (!normalizedDate) return undefined;

return `${normalizedDate}T${normalizeSearchTime(time)}`;
return `${normalizedDate}T${normalizeSearchTime(time, defaultTime)}`;
}

function normalizeSearchTime(time?: string) {
return /^([01]\d|2[0-3]):[0-5]\d$/.test(time ?? '') ? time : '00:00';
function normalizeSearchTime(time?: string, defaultTime = '00:00') {
return /^([01]\d|2[0-3]):[0-5]\d$/.test(time ?? '') ? time : defaultTime;
}

export function toDailyMessage(response?: ApiResponseDailyMessageResponseDto): DailyMessage {
Expand All @@ -238,6 +248,7 @@ export function toScheduleDetail(response: ScheduleDetailResponse): ScheduleDeta
id: response.schedule_id ?? 0,
title: response.title ?? '',
date: normalizeDateForView(response.date),
endDate: normalizeDateForView(response.end_date),
startTime: normalizeTimeToMinute(response.start_time ?? ''),
endTime: normalizeTimeToMinute(response.end_time ?? ''),
estimatedMinutes: response.estimated_time ?? null,
Expand Down Expand Up @@ -363,6 +374,7 @@ export function toScheduleCreateResult(response: ScheduleCreateResponse): Schedu
id: detail.id,
title: detail.title,
date: detail.date,
endDate: detail.endDate,
startTime: detail.startTime,
endTime: detail.endTime,
estimatedMinutes: detail.estimatedMinutes,
Expand Down Expand Up @@ -393,6 +405,7 @@ export function toScheduleCreateRequest(input: ScheduleCreateInput): ScheduleCre
condition_tag: conditionTagToCreateDtoMap[input.conditionTagId],
personal_tags: input.personalTags,
date: normalizeDateForRequest(input.date),
end_date: normalizeDateForRequest(input.endDate),
start_time: input.startTime,
end_time: input.endTime,
estimated_time: input.estimatedMinutes,
Expand All @@ -413,13 +426,17 @@ export function toScheduleCreateRequest(input: ScheduleCreateInput): ScheduleCre
};
}

export function toScheduleUpdateRequest(input: ScheduleUpdateInput): ScheduleUpdateRequest {
export function toScheduleUpdateRequest(
input: ScheduleUpdateInput,
): ScheduleUpdateRequestWithNullableEndDate {
return {
title: input.title,
condition_tag:
input.conditionTagId == null ? undefined : conditionTagToUpdateDtoMap[input.conditionTagId],
personal_tags: input.personalTags,
date: normalizeDateForRequest(input.date),
end_date: input.endDate == null ? input.endDate : normalizeDateForRequest(input.endDate),
end_date_present: input.endDate === undefined ? undefined : true,
start_time: input.startTime,
end_time: input.endTime,
estimated_time: input.estimatedMinutes,
Expand Down
Loading
Loading