Skip to content
Open
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
110 changes: 110 additions & 0 deletions docs/superpowers/specs/2026-07-20-logic-presentation-seam-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Phase 2 — 로직↔프레젠테이션 이음새 정리

## 배경

YoGo 리뉴얼은 4단계로 나뉜다: (1) 플랫폼 업그레이드(RN 0.81+/New Arch),
(2) 로직 분리·응집도, (3) Tamagui 디자인 시스템, (4) 비주얼 리뉴얼.
이 문서는 **(2)** 의 spec이다.

Phase 3~4가 프레젠테이션 계층을 통째로 Tamagui로 다시 쓴다. 그래서 Phase 2는
프레젠테이션 경계를 다듬는 데 시간을 쓰지 않는다. 대신 **살아남는 계층**
(로직·데이터 훅·데이터 흐름)을 깨끗이 해서, Phase 3의 재작성이 얇은 로직
계층에 그냥 꽂히게 한다.

이미 앞선 작업(PR #168)에서 repository 계층, `useCitySearch`,
`useScheduleForm`, timeZone/scheduleForm utils로 많은 로직이 분리되어 있다.
이 브랜치는 그 위에 스택된다.

## 목표

- 화면이 데이터를 직접 오케스트레이션하지 않는다 (fetch·상태·새로고침을 훅 뒤로).
- 전역 불린 캐시 무효화(`PopContext` + `setPop(true)`) 제거.
- `selectedDay` prop drilling(6~7단계) 제거.
- 컴포넌트에 남은 계산 로직을 훅/util로.

## 비목표 (Phase 3로 미룸)

- atom/molecule 재분류 (`ModalTimeInfo`는 폴더 그대로 두고 로직만 뺀다).
- `SwipeContent`→`ScheduleCard` 계층 역전 수정.
- 스타일링/디자인 토큰.

## 설계

### 1. 데이터 훅

```
useSchedules(selectedDay: string)
→ { schedules, markedDates, removeSchedule }
```
캡슐화: `initScheduleTable` · `findSchedulesByDay` · `findScheduleDays`
→ `splitScheduleDays` / `getDatesForWeekdays` → `markedDates` · 삭제+알림 정리.
`templates/Home`의 `initDB`/`markedDB`/`onDeleteTarget` 오케스트레이션을 대체한다.

```
useTimezones()
→ { timezones, addTimezone, removeTimezone }
```
캡슐화: `initTimezoneTable` · `findTimezones` · `addTimezone` · `removeTimezone`
+ `cardState`. `templates/TimeZone`의 오케스트레이션을 대체한다.

두 훅 모두 화면은 "데이터 + 액션"만 구조분해로 받는다.

### 2. 변경 통지 스토어 (`PopContext` 대체)

무의존 모듈 레벨 이벤트 스토어:

```
scheduleStore
subscribe(listener): unsubscribe
emitChanged(): void
```

`useSchedules`가 마운트 시 구독하고 `emitChanged`가 오면 재조회한다.
일정 저장/수정/삭제 흐름(`useScheduleForm`, `useSchedules.removeSchedule`)이
`emitChanged()`를 부른다. `PopContext`와 `setPop(true)` 전역 불린은 삭제한다.
timezone도 필요하면 같은 패턴의 `timezoneStore`를 둔다(추가/삭제가 같은 화면
안이라 우선은 훅 내부 상태로 충분하면 생략).

### 3. `SelectedDayContext`

```
SelectedDayProvider (Home/Main 상단)
useSelectedDay() → { selectedDay, setSelectedDay }
```

`selectedDay`를 props로 6~7단계 흘려보내던 것을 context 직접 읽기로 바꾼다.
깊은 소비자(`ModalTimeInfo` 등)가 context에서 읽는다. 작고 집중된 context이며
Phase 3에서도 생존한다.

### 4. 컴포넌트 로직 추출

- `atoms/ModalTimeInfo`: `useTimeZone` 기반 시각 계산을 훅/util로 빼내
프레젠테이션만 남긴다(계산된 문자열을 받는다). 폴더 위치·이름은 유지.
- 컴포넌트 본문에 남은 날짜/포맷 계산이 있으면 util로 옮긴다.

### 데이터 흐름 (Phase 2 후)

```
SQLite ─ repository(db/*) ─ 데이터 훅(useSchedules/useTimezones) ─ 화면(얇음)
scheduleStore(변경 이벤트) ← 저장/수정/삭제가 통지
selectedDay: SelectedDayContext → 깊은 소비자가 직접 읽음
```

## 검증

- 각 데이터 훅: `react-test-renderer` 렌더 하네스 + db mock 테스트.
- 추출 util/store: 순수 단위 테스트.
- 전체 tsc 0 / lint 0 / jest green 유지.
- 실기기(시뮬레이터)에서 홈 일정 목록·삭제, 타임존 추가·삭제, 저장 후
자동 반영을 회귀 확인.

## 구현 순서 (TDD)

1. `scheduleStore` (이벤트 버스) — 순수, 테스트 먼저.
2. `useTimezones()` — 테스트 → `templates/TimeZone` 이관.
3. `useSchedules(selectedDay)` — 테스트 → `templates/Home` 이관, store 연결.
4. 저장/수정/삭제 흐름을 `emitChanged()`로 연결, `PopContext` 제거.
5. `SelectedDayContext` — provider + 깊은 소비자 이관, drilling 제거.
6. `ModalTimeInfo` 로직 추출 → 프레젠테이션화.
7. tsc/lint/jest + 실기기 검증.
10 changes: 5 additions & 5 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import SplashScreen from 'react-native-splash-screen';
import { ThemeProvider } from 'styled-components/native';
import { PortalProvider } from '@gorhom/portal';
import { theme } from 'styles/theme';
import { FirstLaunchProvider, PopProvider } from 'context';
import { FirstLaunchProvider, SelectedDayProvider } from 'context';
import RootStack from './RootState';

function App() {
Expand All @@ -13,13 +13,13 @@ function App() {

return (
<FirstLaunchProvider>
<PortalProvider>
<PopProvider>
<SelectedDayProvider>
<PortalProvider>
<ThemeProvider theme={theme}>
<RootStack />
</ThemeProvider>
</PopProvider>
</PortalProvider>
</PortalProvider>
</SelectedDayProvider>
</FirstLaunchProvider>
);
}
Expand Down
48 changes: 8 additions & 40 deletions src/components/atoms/ModalTimeInfo/index.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import React from 'react';
import { Title, SubTitle } from 'components';
import { IconRight } from 'assets';
import { parseCity, formatCityName, toFormat12Hour } from 'utils';
import { getModalTimeInfo } from 'utils';
import { ITargetProps, ICurProps } from 'types';
import { useTimeZone } from 'hooks';
import { useSelectedDay } from 'context';
import * as S from './style';

interface IModalTimeProps {
timeData: {
target: ITargetProps;
cur: ICurProps;
};
selectedDay: string;
}

interface IModalTimerProps {
Expand All @@ -21,56 +20,25 @@ interface IModalTimerProps {
}

function ModalTimer({ city, date, time }: IModalTimerProps) {
const [t] = time.split(' ');
return (
<S.Wrapper>
<Title isEnable={true} text={city} size={20} />
<SubTitle isEnable={true} text={date} />
<Title
isEnable={true}
text={toFormat12Hour({ day: date, time: t })}
size={17}
/>
<Title isEnable={true} text={time} size={17} />
</S.Wrapper>
);
}

export function ModalTimeInfo({ timeData, selectedDay }: IModalTimeProps) {
const { target, cur } = timeData;
export function ModalTimeInfo({ timeData }: IModalTimeProps) {
const { selectedDay } = useSelectedDay();

const { getTargetTime, formatTo12Hour } = useTimeZone();

const { TARGET_CITY } = target;
const { CUR_TIME, CUR_CITY } = cur;

const [date, time] = getTargetTime({
currentTime: `${selectedDay} ${CUR_TIME}`,
targetTimeZone: TARGET_CITY,
}).split(' ');

const [targetDay, targetTime] = formatTo12Hour({
date,
time,
});

const [curDay, curTime] = formatTo12Hour({
date: selectedDay,
time: CUR_TIME,
});
const { target, cur } = getModalTimeInfo({ ...timeData, selectedDay });

return (
<S.Container>
<ModalTimer
city={formatCityName(parseCity({ city: TARGET_CITY }))}
date={targetDay}
time={targetTime}
/>
<ModalTimer city={target.city} date={target.date} time={target.time} />
<IconRight />
<ModalTimer
city={formatCityName(parseCity({ city: CUR_CITY }))}
date={curDay}
time={curTime}
/>
<ModalTimer city={cur.city} date={cur.date} time={cur.time} />
</S.Container>
);
}
9 changes: 2 additions & 7 deletions src/components/molecules/ModalTime/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,12 @@ interface IModalTimeProps {
cur: ICurProps;
};
leftTime: string;
selectedDay: string;
}

export function ModalTime({
timeData,
leftTime,
selectedDay,
}: IModalTimeProps) {
export function ModalTime({ timeData, leftTime }: IModalTimeProps) {
return (
<S.Container>
<ModalTimeInfo timeData={timeData} selectedDay={selectedDay} />
<ModalTimeInfo timeData={timeData} />
<S.Text>{leftTime}</S.Text>
</S.Container>
);
Expand Down
4 changes: 1 addition & 3 deletions src/components/molecules/SwipeContent/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,13 @@ import * as S from './style';

interface ISwipeContentProps {
data: IScheduleProps[];
selectedDay: string;
onDeleteTarget: (id: number) => Promise<void>;
onEditTarget: (item: IScheduleProps) => void;
}
export const SwipeContent = ({
data,
onDeleteTarget,
onEditTarget,
selectedDay,
}: ISwipeContentProps) => {
const { deleteRow } = useSwipeList({
listData: data,
Expand All @@ -26,7 +24,7 @@ export const SwipeContent = ({

const renderItem = ({ item, index }: any) => (
<S.Container key={index}>
<ScheduleCard schedule={item} selectedDay={selectedDay} />
<ScheduleCard schedule={item} />
</S.Container>
);

Expand Down
1 change: 0 additions & 1 deletion src/components/organisms/AgendaBox/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ export function AgendaBox({
data={filteredSchedule}
onDeleteTarget={onDeleteTarget}
onEditTarget={onEditTarget}
selectedDay={selectedDay}
/>
)}
</S.Content>
Expand Down
11 changes: 4 additions & 7 deletions src/components/organisms/DetailModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,23 @@ import React from 'react';
import Modal from 'react-native-modal';
import { useTimeZone } from 'hooks';
import { ModalHeader, ModalTime, ModalMemo } from 'components';
import { useSelectedDay } from 'context';
import { IScheduleProps } from 'types';
import * as S from './style';

interface IDetailModalProps {
isVisible: boolean;
onCloseDetailPress: () => void;
selectedDay: string;
schedule: IScheduleProps;
}

export function DetailModal({
isVisible,
onCloseDetailPress,
selectedDay,
schedule,
}: IDetailModalProps) {
const { selectedDay } = useSelectedDay();

const {
TITLE,
DESCRIPTION,
Expand Down Expand Up @@ -63,11 +64,7 @@ export function DetailModal({
<S.Container>
<S.Content>
<ModalHeader tagColor={TAG_COLOR} title={TITLE} />
<ModalTime
timeData={timeData}
leftTime={leftTime}
selectedDay={selectedDay}
/>
<ModalTime timeData={timeData} leftTime={leftTime} />
<ModalMemo description={DESCRIPTION} />
</S.Content>
<S.Wrapper onPress={onCloseDetailPress}>
Expand Down
3 changes: 0 additions & 3 deletions src/components/organisms/ScheduleCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,10 @@ import * as S from './style';

interface IScheduleCardProps {
schedule: IScheduleProps;
selectedDay: string;
}

export const ScheduleCard = React.memo(function ScheduleCard({
schedule,
selectedDay,
}: IScheduleCardProps) {
const {
key,
Expand Down Expand Up @@ -88,7 +86,6 @@ export const ScheduleCard = React.memo(function ScheduleCard({
<DetailModal
isVisible={isVisible}
onCloseDetailPress={onCloseDetailPress}
selectedDay={selectedDay}
schedule={schedule}
/>
</Portal>
Expand Down
6 changes: 1 addition & 5 deletions src/components/organisms/SettingSchedule/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useContext } from 'react';
import React from 'react';
import {
TextInput,
SearchTarget,
Expand All @@ -9,14 +9,11 @@ import {
} from 'components';
import { IHandelScheduleProps } from 'types';
import { useScheduleForm } from 'hooks';
import { PopContext } from 'context';
import * as S from './style';

export function SettingSchedule({ navigation, route }: IHandelScheduleProps) {
const { title, item } = route.params;

const { setPop } = useContext(PopContext);

const {
inputs,
tagList,
Expand All @@ -43,7 +40,6 @@ export function SettingSchedule({ navigation, route }: IHandelScheduleProps) {

if (!isSaved) return;

setPop(true);
navigation.pop();
};

Expand Down
3 changes: 1 addition & 2 deletions src/components/organisms/TimeZoneList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { Dispatch, SetStateAction } from 'react';
import React from 'react';
import { Animated } from 'react-native';
import { SwipeListView } from 'react-native-swipe-list-view';
import { TimeZoneCard, HiddenDelete, RenderEmptyData } from 'components';
Expand All @@ -14,7 +14,6 @@ interface IItemProps {

interface ITimeZoneListProps {
cardState: Array<ICityProps>;
setCardState: Dispatch<SetStateAction<Array<ICityProps>>>;
onDeleteTarget: (id: number) => Promise<void>;
}

Expand Down
Loading