diff --git a/__tests__/Components.test.js b/__tests__/Components.test.js
index e1753fd..f31539e 100644
--- a/__tests__/Components.test.js
+++ b/__tests__/Components.test.js
@@ -7,6 +7,8 @@ import ActionMenuItem from '../src/components/ActionMenuItem';
import StatusBadge from '../src/components/StatusBadge';
import FloatingActionButton from '../src/components/FloatingActionButton';
import TimePickerModal from '../src/components/modals/TimePickerModal';
+import RecordPrintModal from '../src/components/modals/RecordPrintModal';
+import ScheduleEditModal from '../src/components/modals/ScheduleEditModal';
describe('공통 컴포넌트 단위 테스트', () => {
test('Separator 컴포넌트가 오류 없이 렌더링되어야 함', () => {
@@ -124,5 +126,85 @@ describe('공통 컴포넌트 단위 테스트', () => {
expect(visibleModal).toBeDefined();
expect(visibleModal.toJSON()).toBeTruthy();
});
+
+ test('RecordPrintModal 컴포넌트가 visible=false 및 true일 때 정상 렌더링되어야 함', () => {
+ let hiddenModal;
+ let studentModal;
+ let recordsModal;
+ const mockStudent = { id: 's1', name: '홍길동', school_grade: '중2' };
+ const mockRecords = [{ id: 'r1', class_date: '2026-04-10', student_id: 's1' }];
+ act(() => {
+ hiddenModal = renderer.create(
+
+ );
+ studentModal = renderer.create(
+
+ );
+ recordsModal = renderer.create(
+
+ );
+ });
+ expect(hiddenModal).toBeDefined();
+ expect(studentModal).toBeDefined();
+ expect(studentModal.toJSON()).toBeTruthy();
+ expect(recordsModal).toBeDefined();
+ expect(recordsModal.toJSON()).toBeTruthy();
+ });
+
+ test('ScheduleEditModal 컴포넌트가 visible=false 및 true일 때 정상 렌더링되어야 함', () => {
+ let hiddenModal;
+ let visibleModal;
+ const mockStudents = [
+ { id: 's1', name: '홍길동', school_grade: '고1', mobile_phone: '010-1234-5678' },
+ ];
+ act(() => {
+ hiddenModal = renderer.create(
+
+ );
+ visibleModal = renderer.create(
+
+ );
+ });
+ expect(hiddenModal).toBeDefined();
+ expect(visibleModal).toBeDefined();
+ expect(visibleModal.toJSON()).toBeTruthy();
+ });
});
diff --git a/log/devlog.md b/log/devlog.md
index 7a6382a..99003d5 100644
--- a/log/devlog.md
+++ b/log/devlog.md
@@ -5,6 +5,37 @@
---
+## 2026-11-13: 전체 UI/UX 5단계 순차 개선 (동적 테마 반응성, 시간 피커 통합, 주간시간표·학생상세 UX, 공통 모달 2종 분리) (by Antigravity)
+
+* **반영 내역 (What landed)**:
+ * **Phase 1 (동적 테마 전환 반응성 복구)**:
+ * 8개 화면([HomeScreen.js](../src/screens/HomeScreen.js), [WeeklyPlanScreen.js](../src/screens/WeeklyPlanScreen.js), [CalendarScreen.js](../src/screens/CalendarScreen.js), [StudentListScreen.js](../src/screens/StudentListScreen.js), [SettingsScreen.js](../src/screens/SettingsScreen.js), [StudentDetailScreen.js](../src/screens/StudentDetailScreen.js), [ClassRecordScreen.js](../src/screens/ClassRecordScreen.js), [HelpScreen.js](../src/screens/HelpScreen.js)) 및 공용 컴포넌트([Separator.js](../src/components/Separator.js), [ConfirmModal.js](../src/components/ConfirmModal.js))의 정적 `StyleSheet.create`를 `useMemo(() => createStyles(theme), [theme])` 구조로 전환하여 설정 화면 프리셋 전환 시 즉시 반영되도록 개선.
+ * **Phase 2 (시간 피커 통합 및 오전 12시 버그 수정)**:
+ * [ClassRecordScreen.js](../src/screens/ClassRecordScreen.js)의 중복 인라인 시간 피커(~180줄)를 제거하고 공용 [TimePickerModal.js](../src/components/modals/TimePickerModal.js)로 통합.
+ * **Phase 3 (주간시간표 모바일 UX 강화)**:
+ * [WeeklyPlanScreen.js](../src/screens/WeeklyPlanScreen.js)의 타임라인 `selectedDayOffset` 초기값을 오늘 요일(월=0~일=6)로 자동 포커스하고, 표 헤더 및 타임라인 요일 탭에 오늘 날짜 강조(`dayHeaderCellToday`, `dayTabPillToday`) 적용.
+ * 주간 표 열 너비(`68 -> 76`, `tableInner.minWidth: 500`) 및 셀 폰트(`cellItemTitle: 10.5`, `cellItemSubject: 9.5`, `cellItemNote: 9`) 확대.
+ * `handleMakeCall`이 다중 연락처 매칭 시 `Alert.alert('통화할 번호 선택', ...)` 선택창을 표시하도록 개선.
+ * **Phase 4 (학생 상세 입력 안정성 및 오프라인 배너)**:
+ * [StudentDetailScreen.js](../src/screens/StudentDetailScreen.js)에 `beforeRemove` + `ConfirmModal` 기반 미저장 변경사항 이탈 방지 가드 추가(저장/삭제 완료 및 휴회/재수강 즉시 저장 시 정상 이탈 허용).
+ * 외부 URL `ImageBackground` 배너를 오프라인 테마 반응형 배너 카드(`styles.bannerCard`)로 교체하고, 신규 등록 모드에서도 초기 수강 상태(`active`/`paused`) 전환 지원.
+ * **Phase 5 (공통 모달 분리 및 단위 테스트)**:
+ * [RecordPrintModal.js](../src/components/modals/RecordPrintModal.js) 신규 분리 후 [StudentListScreen.js](../src/screens/StudentListScreen.js), [ClassRecordScreen.js](../src/screens/ClassRecordScreen.js), [StudentDetailScreen.js](../src/screens/StudentDetailScreen.js) 연동.
+ * [ScheduleEditModal.js](../src/components/modals/ScheduleEditModal.js) 신규 분리(이름·학교·전화번호 확장 검색 포함) 후 [WeeklyPlanScreen.js](../src/screens/WeeklyPlanScreen.js) 경량화(2,765줄 -> 2,097줄).
+ * [__tests__/Components.test.js](../__tests__/Components.test.js)에 `RecordPrintModal`, `ScheduleEditModal` 렌더링 단위 테스트 2건 추가.
+
+* **검증 수치 (Numbers)**:
+ * 올인원 품질 게이트 (`cmd /c npm run gate`): 100% 통과
+ * ESLint 무결성: 에러 0건, 경고 0건
+ * Jest 단위 테스트: 6개 스위트, 33개 테스트 전체 통과 (100% PASS)
+ * 정기 프로젝트 건전성 정밀 감사: **100점 / 100점** 달성 (초거대 위험 파일 0건, 공통 컴포넌트 11개)
+
+* **미해결 과제 (Still open)**:
+ * [docs/gaps.md](file:///d:/development/mydiary/docs/gaps.md)의 `[G1]` HWPX 복수 표 서식 레이아웃 프리셋(표준형, 서술형, 결재란 포함형) 다변화 작업 대기 중.
+ * [StudentDetailScreen.js](../src/screens/StudentDetailScreen.js)(1,587줄) 및 [WeeklyPlanScreen.js](../src/screens/WeeklyPlanScreen.js)(2,097줄) 추가 세부 섹션 분리 선택 사항.
+
+---
+
## 2026-11-02: v2.5.0 공식 마이너 릴리즈 마일스톤 버전 5개 파일 동기화 및 릴리즈 태깅 (by Antigravity)
* **반영 내역 (What landed)**:
diff --git a/src/components/ConfirmModal.js b/src/components/ConfirmModal.js
index d0962ea..912dead 100644
--- a/src/components/ConfirmModal.js
+++ b/src/components/ConfirmModal.js
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useMemo } from 'react';
import {
Modal,
View,
@@ -8,7 +8,8 @@ import {
ActivityIndicator,
} from 'react-native';
import { Feather } from '@expo/vector-icons';
-import { theme } from '../theme';
+import { useTheme } from '../context/ThemeContext';
+import { theme as defaultTheme } from '../theme';
/**
* 앱 전반에서 일관된 UX를 제공하는 공통 확인/삭제 모달 컴포넌트
@@ -25,6 +26,10 @@ const ConfirmModal = ({
loading = false,
iconName = isDestructive ? 'alert-triangle' : 'help-circle',
}) => {
+ const { theme: activeTheme } = useTheme();
+ const theme = activeTheme || defaultTheme;
+ const styles = useMemo(() => createStyles(theme), [theme]);
+
return (
StyleSheet.create({
overlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
diff --git a/src/components/Separator.js b/src/components/Separator.js
index 9425b90..2677ec5 100644
--- a/src/components/Separator.js
+++ b/src/components/Separator.js
@@ -1,12 +1,24 @@
import React from 'react';
import { View, StyleSheet } from 'react-native';
-import { theme } from '../theme';
+import { useTheme } from '../context/ThemeContext';
+import { theme as defaultTheme } from '../theme';
/**
* 리스트 아이템 사이 또는 섹션 구분을 위한 공통 구분선 컴포넌트
*/
const Separator = React.memo(({ style }) => {
- return ;
+ const { theme: activeTheme } = useTheme();
+ const theme = activeTheme || defaultTheme;
+
+ return (
+
+ );
});
Separator.displayName = 'Separator';
@@ -14,7 +26,6 @@ Separator.displayName = 'Separator';
const styles = StyleSheet.create({
separator: {
height: 1,
- backgroundColor: theme.colors.outline || '#E2E8F0',
},
});
diff --git a/src/components/modals/RecordPrintModal.js b/src/components/modals/RecordPrintModal.js
new file mode 100644
index 0000000..7b13db9
--- /dev/null
+++ b/src/components/modals/RecordPrintModal.js
@@ -0,0 +1,472 @@
+import React, { useState, useEffect, useMemo } from 'react';
+import {
+ Modal,
+ View,
+ Text,
+ TouchableOpacity,
+ ScrollView,
+ StyleSheet,
+} from 'react-native';
+import { Calendar } from 'react-native-calendars';
+import { Feather } from '@expo/vector-icons';
+import { useTheme } from '../../context/ThemeContext';
+import { theme as defaultTheme } from '../../theme';
+import { getTodayFormatted } from '../../utils/dateUtils';
+import ActionMenuItem from '../ActionMenuItem';
+
+/**
+ * 학생 카드 및 수업 일지 기간별 출력(프린터 인쇄 / PDF 공유) 공통 모달 컴포넌트
+ * @param {object} props - 모달 속성
+ * @param {boolean} props.visible - 모달 표시 여부
+ * @param {() => void} props.onClose - 닫기 콜백
+ * @param {'student'|'records'} [props.printType='records'] - 출력 유형 ('student': 학생 카드, 'records': 수업 일지)
+ * @param {object|null} [props.student] - 대상 학생 객체
+ * @param {Array
- {/* 수업 일지 출력 모달 (기간 선택 + 프린터 인쇄 vs PDF 공유) */}
- setShowTimePicker(false)}
+ />
+
+ {/* 공통 수업 일지 출력 모달 */}
+ setPrintModalVisible(false)}
- statusBarTranslucent
- >
- setPrintModalVisible(false)}
- >
- e.stopPropagation()}
- >
-
-
-
- 수업 일지 보고서 출력
-
- {student?.name || '학생'} 학생의 수업 기록 ({filteredRecordsForPrint.length}건 선택됨)
-
-
- setPrintModalVisible(false)} style={styles.actionModalCloseBtn}>
-
-
-
-
- {/* 기간 필터 선택 칩 */}
- 출력 기간 선택
-
- {
- setPeriodFilter('all');
- setActiveDatePicker(null);
- }}
- >
-
- 전체 기간
-
-
-
- {
- setPeriodFilter('current_month');
- setActiveDatePicker(null);
- }}
- >
-
- 이번 달
-
-
-
- {
- setPeriodFilter('1m');
- setActiveDatePicker(null);
- }}
- >
-
- 최근 1개월
-
-
-
- {
- setPeriodFilter('3m');
- setActiveDatePicker(null);
- }}
- >
-
- 최근 3개월
-
-
-
- {
- setPeriodFilter('custom');
- setActiveDatePicker(null);
- }}
- >
-
- 직접 설정
-
-
-
-
- {/* 직접 설정 선택 시 시작일 ~ 종료일 선택기 */}
- {periodFilter === 'custom' && (
-
-
- setActiveDatePicker(activeDatePicker === 'start' ? null : 'start')}
- >
- 시작일
- {customStartDate}
-
-
- ~
-
- setActiveDatePicker(activeDatePicker === 'end' ? null : 'end')}
- >
- 종료일
- {customEndDate}
-
-
-
- {/* 날짜 선택 인라인 캘린더 */}
- {activeDatePicker && (
-
-
-
- {activeDatePicker === 'start' ? '시작 날짜 선택' : '종료 날짜 선택'}
-
-
- {
- if (activeDatePicker === 'start') {
- setCustomStartDate(day.dateString);
- if (day.dateString > customEndDate) {
- setCustomEndDate(day.dateString);
- }
- } else {
- setCustomEndDate(day.dateString);
- if (day.dateString < customStartDate) {
- setCustomStartDate(day.dateString);
- }
- }
- setActiveDatePicker(null);
- }}
- theme={{
- selectedDayBackgroundColor: theme.colors.primary,
- todayTextColor: theme.colors.primary,
- arrowColor: theme.colors.primary,
- }}
- />
-
- )}
-
- )}
-
- {/* 선택된 대상 요약 배너 */}
-
-
-
- 조회: {periodTitle} ({filteredRecordsForPrint.length}회차 기록)
-
-
-
- {/* 메뉴 1: 무선/유선 프린터로 인쇄 */}
- {
- setPrintModalVisible(false);
- await printClassRecords(student, filteredRecordsForPrint, periodTitle);
- }}
- />
-
- {/* 메뉴 2: PDF 파일 공유 (카톡/메시지) */}
- {
- setPrintModalVisible(false);
- await shareClassRecords(student, filteredRecordsForPrint, periodTitle);
- }}
- />
-
-
-
-
+ onClose={() => setPrintModalVisible(false)}
+ printType="records"
+ student={student}
+ records={allRecords}
+ onPrintRecords={(stud, recs, title) => printClassRecords(stud, recs, title)}
+ onShareRecords={(stud, recs, title) => shareClassRecords(stud, recs, title)}
+ />
);
}
-const styles = StyleSheet.create({
+const createStyles = (theme) => StyleSheet.create({
container: {
flex: 1,
backgroundColor: theme.colors.surfaceVariant,
@@ -1338,118 +863,6 @@ const styles = StyleSheet.create({
borderRadius: theme.roundness,
overflow: 'hidden',
},
- inlineTimePicker: {
- marginTop: 8,
- borderWidth: 1,
- borderColor: theme.colors.secondaryContainer,
- borderRadius: theme.roundness,
- backgroundColor: theme.colors.white,
- padding: 12,
- },
- timePickerSectionTitle: {
- fontSize: 12,
- fontWeight: '700',
- color: theme.colors.textSecondary,
- marginBottom: 6,
- marginTop: 6,
- },
- quickPresetScroll: {
- paddingVertical: 2,
- marginBottom: 8,
- },
- quickPresetChip: {
- paddingHorizontal: 12,
- paddingVertical: 6,
- borderRadius: 16,
- backgroundColor: theme.colors.surfaceVariant,
- marginRight: 6,
- borderWidth: 1,
- borderColor: theme.colors.outline + '40',
- },
- quickPresetChipActive: {
- backgroundColor: theme.colors.primary,
- borderColor: theme.colors.primary,
- },
- quickPresetText: {
- fontSize: 12.5,
- color: theme.colors.textSecondary,
- fontWeight: '600',
- },
- quickPresetTextActive: {
- color: theme.colors.white,
- fontWeight: 'bold',
- },
- timeAmpmRow: {
- flexDirection: 'row',
- backgroundColor: theme.colors.surfaceVariant,
- borderRadius: 8,
- padding: 3,
- marginBottom: 8,
- marginTop: 4,
- },
- timeAmpmTab: {
- flex: 1,
- paddingVertical: 7,
- alignItems: 'center',
- borderRadius: 6,
- },
- timeAmpmTabActive: {
- backgroundColor: theme.colors.white,
- shadowColor: '#000',
- shadowOffset: { width: 0, height: 1 },
- shadowOpacity: 0.1,
- elevation: 2,
- },
- timeAmpmTabText: {
- fontSize: 13,
- color: theme.colors.textSecondary,
- fontWeight: '600',
- },
- timeAmpmTabTextActive: {
- color: theme.colors.primary,
- fontWeight: 'bold',
- },
- timeGridRow: {
- flexDirection: 'row',
- flexWrap: 'wrap',
- gap: 6,
- marginBottom: 10,
- },
- timeGridBtn: {
- flexBasis: '22.5%',
- flexGrow: 1,
- paddingVertical: 7,
- borderRadius: 8,
- alignItems: 'center',
- backgroundColor: theme.colors.surfaceVariant + '80',
- borderWidth: 1,
- borderColor: theme.colors.outline + '40',
- },
- timeGridBtnActive: {
- backgroundColor: theme.colors.secondaryContainer,
- borderColor: theme.colors.primary,
- },
- timeGridBtnText: {
- fontSize: 12,
- color: theme.colors.textPrimary,
- fontWeight: '500',
- },
- timeGridBtnTextActive: {
- color: theme.colors.onSecondaryContainer,
- fontWeight: 'bold',
- },
- timeConfirmBtn: {
- marginTop: 6,
- backgroundColor: theme.colors.primary,
- paddingVertical: 9,
- borderRadius: 8,
- alignItems: 'center',
- },
- timeConfirmBtnText: {
- color: theme.colors.white,
- fontSize: 13,
- fontWeight: 'bold',
- },
modalInput: {
borderWidth: 1,
borderColor: theme.colors.outline,
@@ -1489,160 +902,6 @@ const styles = StyleSheet.create({
fontWeight: 'bold',
fontSize: 15,
},
- actionModalOverlay: {
- flex: 1,
- backgroundColor: 'rgba(0, 0, 0, 0.45)',
- justifyContent: 'flex-end',
- },
- actionModalContent: {
- backgroundColor: theme.colors.white,
- borderTopLeftRadius: 24,
- borderTopRightRadius: 24,
- padding: 24,
- paddingBottom: 24,
- maxHeight: '90%',
- shadowColor: '#000',
- shadowOffset: { width: 0, height: -4 },
- shadowOpacity: 0.1,
- shadowRadius: 12,
- elevation: 10,
- },
- actionModalHeader: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- marginBottom: 16,
- paddingBottom: 12,
- borderBottomWidth: 1,
- borderBottomColor: theme.colors.surfaceVariant,
- },
- actionModalTitleContainer: {
- flex: 1,
- },
- actionModalTitle: {
- fontSize: 18,
- fontWeight: 'bold',
- color: theme.colors.textPrimary,
- marginBottom: 4,
- },
- actionModalSubtitle: {
- fontSize: 13,
- color: theme.colors.textSecondary,
- },
- actionModalCloseBtn: {
- padding: 4,
- },
- filterSectionLabel: {
- fontSize: 13,
- fontWeight: '700',
- color: theme.colors.textPrimary,
- marginBottom: 10,
- },
- filterChipGroup: {
- flexDirection: 'row',
- flexWrap: 'wrap',
- gap: 8,
- marginBottom: 14,
- },
- filterChip: {
- paddingHorizontal: 12,
- paddingVertical: 7,
- borderRadius: 20,
- backgroundColor: theme.colors.surfaceVariant,
- borderWidth: 1,
- borderColor: 'transparent',
- },
- filterChipActive: {
- backgroundColor: '#EFF6FF',
- borderColor: theme.colors.primary,
- },
- filterChipText: {
- fontSize: 13,
- color: theme.colors.textSecondary,
- fontWeight: '600',
- },
- filterChipTextActive: {
- color: theme.colors.primary,
- fontWeight: '700',
- },
- summaryBadgeBox: {
- flexDirection: 'row',
- alignItems: 'center',
- backgroundColor: theme.colors.secondaryContainer,
- paddingHorizontal: 12,
- paddingVertical: 8,
- borderRadius: 8,
- marginBottom: 16,
- },
- summaryBadgeText: {
- fontSize: 12.5,
- color: theme.colors.primary,
- fontWeight: '600',
- },
- customDateContainer: {
- backgroundColor: theme.colors.surfaceVariant + '60',
- padding: 12,
- borderRadius: 12,
- marginBottom: 14,
- borderWidth: 1,
- borderColor: theme.colors.outline + '40',
- },
- customDateRow: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- },
- customDateBtn: {
- flex: 1,
- backgroundColor: theme.colors.white,
- paddingVertical: 8,
- paddingHorizontal: 10,
- borderRadius: 8,
- borderWidth: 1,
- borderColor: theme.colors.outline,
- alignItems: 'center',
- },
- customDateBtnActive: {
- borderColor: theme.colors.primary,
- backgroundColor: '#EFF6FF',
- },
- customDateLabel: {
- fontSize: 11,
- color: theme.colors.textSecondary,
- marginBottom: 2,
- fontWeight: '600',
- },
- customDateText: {
- fontSize: 13,
- fontWeight: '700',
- color: theme.colors.textPrimary,
- },
- customDateTilde: {
- fontSize: 16,
- fontWeight: 'bold',
- color: theme.colors.textSecondary,
- marginHorizontal: 8,
- },
- inlineCustomCalendar: {
- marginTop: 10,
- backgroundColor: theme.colors.white,
- borderRadius: 10,
- padding: 8,
- borderWidth: 1,
- borderColor: theme.colors.outline + '40',
- },
- inlineCustomCalendarHeader: {
- paddingBottom: 6,
- borderBottomWidth: 1,
- borderBottomColor: theme.colors.surfaceVariant,
- marginBottom: 6,
- alignItems: 'center',
- },
- inlineCustomCalendarTitle: {
- fontSize: 12,
- fontWeight: '700',
- color: theme.colors.primary,
- },
studentBarLeft: {
flex: 1,
},
@@ -1652,10 +911,4 @@ const styles = StyleSheet.create({
emptyFolderIcon: {
marginBottom: 16,
},
- modalScrollContent: {
- paddingBottom: 10,
- },
- summaryBadgeIcon: {
- marginRight: 6,
- },
});
diff --git a/src/screens/HelpScreen.js b/src/screens/HelpScreen.js
index 36ef4c4..5131fcf 100644
--- a/src/screens/HelpScreen.js
+++ b/src/screens/HelpScreen.js
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useState, useMemo } from 'react';
import {
View,
Text,
@@ -9,12 +9,13 @@ import {
} from 'react-native';
import { Feather, MaterialCommunityIcons } from '@expo/vector-icons';
import { useTheme } from '../context/ThemeContext';
-import { theme } from '../theme';
+
import packageJson from '../../package.json';
export default function HelpScreen() {
- // eslint-disable-next-line no-shadow
+
const { theme } = useTheme();
+ const styles = useMemo(() => createStyles(theme), [theme]);
const [activeTab, setActiveTab] = useState('guide'); // 'guide' | 'about' | 'faq'
const appGuides = [
@@ -307,7 +308,7 @@ export default function HelpScreen() {
);
}
-const styles = StyleSheet.create({
+const createStyles = (theme) => StyleSheet.create({
container: {
flex: 1,
backgroundColor: theme.colors.surfaceVariant,
diff --git a/src/screens/HomeScreen.js b/src/screens/HomeScreen.js
index ee6d64c..ebbc317 100644
--- a/src/screens/HomeScreen.js
+++ b/src/screens/HomeScreen.js
@@ -12,11 +12,10 @@ import { useNavigation, useIsFocused } from '@react-navigation/native';
import { Database } from '../database/Database';
import { useTheme } from '../context/ThemeContext';
import { DAY_NAMES, getTodayFormatted } from '../utils/dateUtils';
-import { theme } from '../theme';
export default function HomeScreen() {
- // eslint-disable-next-line no-shadow
const { theme } = useTheme();
+ const styles = useMemo(() => createStyles(theme), [theme]);
const navigation = useNavigation();
const isFocused = useIsFocused();
@@ -438,10 +437,10 @@ export default function HomeScreen() {
);
}
-const styles = StyleSheet.create({
+const createStyles = (theme) => StyleSheet.create({
container: {
flex: 1,
- backgroundColor: '#F8FAFC',
+ backgroundColor: theme.colors.surfaceVariant,
},
scrollContent: {
paddingHorizontal: 16,
@@ -517,7 +516,7 @@ const styles = StyleSheet.create({
marginBottom: 2,
},
highlightText: {
- color: '#2563EB',
+ color: theme.colors.primary,
fontWeight: '800',
},
highlightGreen: {
@@ -598,13 +597,13 @@ const styles = StyleSheet.create({
},
progressBar: {
height: '100%',
- backgroundColor: '#4F46E5',
+ backgroundColor: theme.colors.primary,
borderRadius: 2,
},
metricBottomHint: {
fontSize: 9.5,
fontWeight: '700',
- color: '#4F46E5',
+ color: theme.colors.primary,
marginTop: 2,
},
sectionContainer: {
@@ -633,7 +632,7 @@ const styles = StyleSheet.create({
},
seeAllText: {
fontSize: 12,
- color: '#4F46E5',
+ color: theme.colors.primary,
fontWeight: '700',
},
loader: {
@@ -675,7 +674,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
gap: 6,
- backgroundColor: '#EEF2FF',
+ backgroundColor: theme.colors.secondaryContainer,
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: 8,
@@ -683,7 +682,7 @@ const styles = StyleSheet.create({
emptyAddBtnText: {
fontSize: 12,
fontWeight: '700',
- color: '#4F46E5',
+ color: theme.colors.primary,
},
scheduleCard: {
backgroundColor: '#FFFFFF',
diff --git a/src/screens/SettingsScreen.js b/src/screens/SettingsScreen.js
index 4f4c4d8..10a4c6a 100644
--- a/src/screens/SettingsScreen.js
+++ b/src/screens/SettingsScreen.js
@@ -1,4 +1,4 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useMemo } from 'react';
import {
View,
Text,
@@ -12,12 +12,13 @@ import {
import { Feather } from '@expo/vector-icons';
import { Database } from '../database/Database';
import { GoogleDriveService } from '../services/GoogleDriveService';
-import { theme } from '../theme';
+import { theme as defaultTheme } from '../theme';
import { useTheme } from '../context/ThemeContext';
export default function SettingsScreen({ navigation }) {
const { theme: activeTheme, currentThemeId, setThemeId, presets } = useTheme();
- const currentTheme = activeTheme || theme;
+ const currentTheme = activeTheme || defaultTheme;
+ const styles = useMemo(() => createStyles(currentTheme), [currentTheme]);
const [activeTab, setActiveTab] = useState('backup'); // 'backup' | 'theme'
const [loading, setLoading] = useState(false);
const [userInfo, setUserInfo] = useState(null);
@@ -379,7 +380,7 @@ export default function SettingsScreen({ navigation }) {
{loading && (
-
+
처리 중입니다...
)}
@@ -387,7 +388,7 @@ export default function SettingsScreen({ navigation }) {
);
}
-const styles = StyleSheet.create({
+const createStyles = (theme) => StyleSheet.create({
segmentContainer: {
flexDirection: 'row',
backgroundColor: '#E8ECF0',
diff --git a/src/screens/StudentDetailScreen.js b/src/screens/StudentDetailScreen.js
index 47b5775..036e2cb 100644
--- a/src/screens/StudentDetailScreen.js
+++ b/src/screens/StudentDetailScreen.js
@@ -11,8 +11,6 @@ import {
KeyboardAvoidingView,
Keyboard,
Platform,
- ImageBackground,
- Modal
} from 'react-native';
import { useNavigation, useRoute } from '@react-navigation/native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
@@ -22,22 +20,26 @@ import { printStudentProfile, shareStudentProfile } from '../services/PrintServi
import { Feather } from '@expo/vector-icons';
import { useTheme } from '../context/ThemeContext';
import { getTodayFormatted } from '../utils/dateUtils';
-import { theme } from '../theme';
import StudentStatusModal from '../components/modals/StudentStatusModal';
import TimePickerModal from '../components/modals/TimePickerModal';
-import ActionMenuItem from '../components/ActionMenuItem';
+import RecordPrintModal from '../components/modals/RecordPrintModal';
+import ConfirmModal from '../components/ConfirmModal';
/**
* 학생 개인 프로필, 수강 상태(재원/휴회), 정규 수업 일정 및 특이사항을 등록·수정하는 화면 컴포넌트
* @returns {React.JSX.Element} 학생 상세 정보 화면 요소
*/
export default function StudentDetailScreen() {
- // eslint-disable-next-line no-shadow
const { theme } = useTheme();
+ const styles = useMemo(() => createStyles(theme), [theme]);
const insets = useSafeAreaInsets();
const navigation = useNavigation();
const route = useRoute();
const { studentId } = route.params || {};
+ const initialSnapshotRef = useRef(null);
+ const isSavedRef = useRef(false);
+ const [leaveConfirmVisible, setLeaveConfirmVisible] = useState(false);
+ const [pendingRemoveAction, setPendingRemoveAction] = useState(null);
const isEditMode = !!studentId;
@@ -114,7 +116,52 @@ export default function StudentDetailScreen() {
isKeyboardVisible && {
paddingBottom: Platform.OS === 'android' ? keyboardHeight + 48 : 80,
},
- ], [isKeyboardVisible, keyboardHeight]);
+ ], [styles.scrollContent, isKeyboardVisible, keyboardHeight]);
+
+ const currentSnapshotStr = useMemo(() => JSON.stringify({
+ name: name.trim(),
+ school_grade: schoolGrade.trim() || null,
+ resident_number: residentNumber.trim() || null,
+ address: address.trim() || null,
+ phone_number: phoneNumber.trim() || null,
+ email: email.trim() || null,
+ mobile_phone: mobilePhone.trim() || null,
+ study_method: studyMethod.trim() || null,
+ parent_name: parentName.trim() || null,
+ parent_mobile_phone: parentMobilePhone.trim() || null,
+ default_schedules: defaultSchedules,
+ notes: notes.trim() || null,
+ status,
+ first_enrolled_date: firstEnrolledDate,
+ current_term_number: currentTermNumber,
+ terms,
+ }), [
+ name, schoolGrade, residentNumber, address, phoneNumber, email, mobilePhone,
+ studyMethod, parentName, parentMobilePhone, defaultSchedules, notes,
+ status, firstEnrolledDate, currentTermNumber, terms,
+ ]);
+
+ useEffect(() => {
+ if (!isEditMode && initialSnapshotRef.current === null) {
+ initialSnapshotRef.current = currentSnapshotStr;
+ }
+ }, [isEditMode, currentSnapshotStr]);
+
+ const hasUnsavedChanges =
+ initialSnapshotRef.current !== null && currentSnapshotStr !== initialSnapshotRef.current;
+
+ useEffect(() => {
+ if (!navigation?.addListener) return undefined;
+ const unsubscribe = navigation.addListener('beforeRemove', (e) => {
+ if (isSavedRef.current || !hasUnsavedChanges) {
+ return;
+ }
+ e.preventDefault();
+ setPendingRemoveAction(e.data.action);
+ setLeaveConfirmVisible(true);
+ });
+ return unsubscribe;
+ }, [navigation, hasUnsavedChanges]);
const activeScheduleTime = useMemo(() => {
if (!activeScheduleIdForTime) return '10:00';
@@ -160,7 +207,7 @@ export default function StudentDetailScreen() {
),
});
}
- }, [navigation, isEditMode, theme.colors.primary]);
+ }, [navigation, isEditMode, theme.colors.primary, styles.headerRightBtn]);
// 입력 자동 포맷팅 핸들러
const formatPhoneNumber = (text) => {
@@ -210,15 +257,38 @@ export default function StudentDetailScreen() {
// 수강 상태 및 차수(terms) 로드
const stTerms = Array.isArray(student.terms) && student.terms.length > 0 ? student.terms : [];
- setTerms(stTerms);
- setStatus(student.status || 'active');
- setFirstEnrolledDate(
+ const loadedStatus = student.status || 'active';
+ const loadedFirstEnrolled =
student.first_enrolled_date ||
- student.start_date ||
- stTerms[0]?.start_date ||
- (student.created_at ? student.created_at.split('T')[0] : getTodayFormatted())
- );
- setCurrentTermNumber(student.current_term_number || stTerms.length || 1);
+ student.start_date ||
+ stTerms[0]?.start_date ||
+ (student.created_at ? student.created_at.split('T')[0] : getTodayFormatted());
+ const loadedTermNumber = student.current_term_number || stTerms.length || 1;
+ const loadedSchedules = Array.isArray(student.default_schedules) ? student.default_schedules : [];
+
+ setTerms(stTerms);
+ setStatus(loadedStatus);
+ setFirstEnrolledDate(loadedFirstEnrolled);
+ setCurrentTermNumber(loadedTermNumber);
+
+ initialSnapshotRef.current = JSON.stringify({
+ name: (student.name || '').trim(),
+ school_grade: (student.school_grade || '').trim() || null,
+ resident_number: (student.resident_number || '').trim() || null,
+ address: (student.address || '').trim() || null,
+ phone_number: (student.phone_number || '').trim() || null,
+ email: (student.email || '').trim() || null,
+ mobile_phone: (student.mobile_phone || '').trim() || null,
+ study_method: (student.study_method || '').trim() || null,
+ parent_name: (student.parent_name || '').trim() || null,
+ parent_mobile_phone: (student.parent_mobile_phone || '').trim() || null,
+ default_schedules: loadedSchedules,
+ notes: (student.notes || '').trim() || null,
+ status: loadedStatus,
+ first_enrolled_date: loadedFirstEnrolled,
+ current_term_number: loadedTermNumber,
+ terms: stTerms,
+ });
}
} catch (e) {
console.error('Failed to load student details:', e);
@@ -277,6 +347,15 @@ export default function StudentDetailScreen() {
setStatus('paused');
setTerms(updated.terms);
setCurrentTermNumber(updated.current_term_number);
+ if (initialSnapshotRef.current) {
+ const prev = JSON.parse(initialSnapshotRef.current);
+ initialSnapshotRef.current = JSON.stringify({
+ ...prev,
+ status: 'paused',
+ terms: updated.terms,
+ current_term_number: updated.current_term_number,
+ });
+ }
setPauseModalVisible(false);
Alert.alert('완료', `${name} 학생이 휴회 처리되었습니다.`);
} catch (e) {
@@ -296,6 +375,15 @@ export default function StudentDetailScreen() {
setStatus('active');
setTerms(updated.terms);
setCurrentTermNumber(updated.current_term_number);
+ if (initialSnapshotRef.current) {
+ const prev = JSON.parse(initialSnapshotRef.current);
+ initialSnapshotRef.current = JSON.stringify({
+ ...prev,
+ status: 'active',
+ terms: updated.terms,
+ current_term_number: updated.current_term_number,
+ });
+ }
setResumeModalVisible(false);
Alert.alert('완료', `${name} 학생의 재수강이 시작되었습니다.`);
} catch (e) {
@@ -344,11 +432,13 @@ export default function StudentDetailScreen() {
try {
if (isEditMode) {
await Database.updateStudent(studentId, studentData);
+ isSavedRef.current = true;
Alert.alert('완료', '학생 정보가 수정되었습니다.', [
{ text: '확인', onPress: () => navigation.goBack() },
]);
} else {
await Database.addStudent(studentData);
+ isSavedRef.current = true;
Alert.alert('완료', '새로운 학생이 등록되었습니다.', [
{ text: '확인', onPress: () => navigation.goBack() },
]);
@@ -372,6 +462,7 @@ export default function StudentDetailScreen() {
onPress: async () => {
try {
await Database.deleteStudent(studentId);
+ isSavedRef.current = true;
Alert.alert('완료', '학생 주소록이 삭제되었습니다.', [
{ text: '확인', onPress: () => navigation.goBack() }
]);
@@ -429,12 +520,14 @@ export default function StudentDetailScreen() {
{status === 'paused' ? '현재 휴회 상태입니다' : `현재 ${currentTermNumber}차 수강 진행 중입니다`}
- {status === 'paused'
- ? '새 학기/새 차수로 복귀 시 [재수강 시작]을 누르세요'
- : '수강을 잠시 쉴 경우 [휴회 처리]를 누르세요'}
+ {isEditMode
+ ? status === 'paused'
+ ? '새 학기/새 차수로 복귀 시 [재수강 시작]을 누르세요'
+ : '수강을 잠시 쉴 경우 [휴회 처리]를 누르세요'
+ : '신규 등록 시 초기 수강 상태(재원/휴회)를 전환할 수 있습니다'}
- {isEditMode && (
+ {isEditMode ? (
status === 'paused' ? (
휴회 처리
)
+ ) : (
+ setStatus((prev) => (prev === 'paused' ? 'active' : 'paused'))}
+ accessibilityRole="button"
+ accessibilityLabel="초기 수강 상태 전환"
+ >
+
+
+ {status === 'paused' ? '재원으로 변경' : '휴회로 변경'}
+
+
)}
@@ -819,17 +932,15 @@ export default function StudentDetailScreen() {
/>
- {/* Image Banner */}
-
-
-
- 기록은 성장의 밑거름입니다.
-
-
+ {/* 오프라인 테마 배너 카드 */}
+
+
+
+
+
+ 기록은 성장의 밑거름입니다.
+ 학생의 배움과 성장을 차곡차곡 기록해보세요.
+
@@ -860,56 +971,41 @@ export default function StudentDetailScreen() {
)}
{/* 학생 카드 출력 모달 (프린터 인쇄 vs PDF 공유) */}
- setPrintModalVisible(false)}
- >
- setPrintModalVisible(false)}
- >
-
-
-
- 학생 정보 카드 출력
- {name || '학생'} 학생의 인적사항 양식
-
- setPrintModalVisible(false)} style={styles.modalCloseBtn}>
-
-
-
-
- {/* 메뉴 1: 무선/유선 프린터로 인쇄 */}
- {
- setPrintModalVisible(false);
- await printStudentProfile(getCurrentStudentData());
- }}
- />
+ onClose={() => setPrintModalVisible(false)}
+ printType="student"
+ student={getCurrentStudentData()}
+ onPrintStudent={async (targetStudent) => {
+ await printStudentProfile(targetStudent);
+ }}
+ onShareStudent={async (targetStudent) => {
+ await shareStudentProfile(targetStudent);
+ }}
+ />
- {/* 메뉴 2: PDF 파일 공유 (카톡/메시지) */}
- {
- setPrintModalVisible(false);
- await shareStudentProfile(getCurrentStudentData());
- }}
- />
-
-
-
+ {/* 미저장 변경사항 이탈 확인 모달 */}
+ {
+ setLeaveConfirmVisible(false);
+ isSavedRef.current = true;
+ if (pendingRemoveAction) {
+ navigation.dispatch(pendingRemoveAction);
+ } else {
+ navigation.goBack();
+ }
+ }}
+ onCancel={() => {
+ setLeaveConfirmVisible(false);
+ setPendingRemoveAction(null);
+ }}
+ />
{/* 휴회 처리 입력 모달 */}
StyleSheet.create({
container: {
flex: 1,
backgroundColor: theme.colors.surfaceVariant,
@@ -1115,31 +1211,38 @@ const styles = StyleSheet.create({
fontSize: 14,
color: theme.colors.textPrimary,
},
- bannerContainer: {
+ bannerCard: {
marginTop: 24,
marginBottom: 16,
- height: 128,
+ padding: 18,
borderRadius: theme.roundness,
- overflow: 'hidden',
+ backgroundColor: theme.colors.secondaryContainer,
+ borderWidth: 1,
+ borderColor: theme.colors.outlineVariant || theme.colors.outline,
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 14,
},
- bannerImage: {
- width: '100%',
- height: '100%',
- justifyContent: 'flex-end',
+ bannerIconWrap: {
+ width: 44,
+ height: 44,
+ borderRadius: 12,
+ backgroundColor: theme.colors.surface,
+ alignItems: 'center',
+ justifyContent: 'center',
},
- bannerOverlay: {
- position: 'absolute',
- bottom: 16,
- left: 16,
- zIndex: 20,
+ bannerTextWrap: {
+ flex: 1,
},
- bannerText: {
- color: theme.colors.white,
- fontSize: 18,
+ bannerTitle: {
+ fontSize: 15,
fontWeight: 'bold',
- textShadowColor: 'rgba(0, 0, 0, 0.4)',
- textShadowOffset: { width: 0, height: 1 },
- textShadowRadius: 4,
+ color: theme.colors.onSecondaryContainer || theme.colors.textPrimary,
+ marginBottom: 3,
+ },
+ bannerSubtitle: {
+ fontSize: 12,
+ color: theme.colors.textSecondary,
},
bottomFixedBar: {
position: 'absolute',
@@ -1191,60 +1294,12 @@ const styles = StyleSheet.create({
fontWeight: 'bold',
fontSize: 14,
},
- modalOverlay: {
- flex: 1,
- backgroundColor: 'rgba(0, 0, 0, 0.45)',
- justifyContent: 'flex-end',
- },
- modalContent: {
- backgroundColor: theme.colors.white,
- borderTopLeftRadius: 24,
- borderTopRightRadius: 24,
- padding: 24,
- paddingBottom: 36,
- shadowColor: '#000',
- shadowOffset: { width: 0, height: -4 },
- shadowOpacity: 0.1,
- shadowRadius: 12,
- elevation: 10,
- },
- modalHeader: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- marginBottom: 20,
- paddingBottom: 14,
- borderBottomWidth: 1,
- borderBottomColor: theme.colors.surfaceVariant,
- },
- modalTitleContainer: {
- flex: 1,
- },
- modalTitle: {
- fontSize: 18,
- fontWeight: 'bold',
- color: theme.colors.textPrimary,
- marginBottom: 4,
- },
- modalSubtitle: {
- fontSize: 13,
- color: theme.colors.textSecondary,
- },
- modalCloseBtn: {
- padding: 4,
- },
headerBtnIcon: {
marginRight: 4,
},
sectionHeaderMargin: {
marginTop: 24,
},
- bannerImageBorder: {
- borderRadius: 20,
- },
- pdfShareIconBadge: {
- backgroundColor: '#E0F2FE',
- },
addSchedBtn: {
flexDirection: 'row',
alignItems: 'center',
diff --git a/src/screens/StudentListScreen.js b/src/screens/StudentListScreen.js
index 85cf732..dfe9a71 100644
--- a/src/screens/StudentListScreen.js
+++ b/src/screens/StudentListScreen.js
@@ -9,11 +9,9 @@ import {
SafeAreaView,
ActivityIndicator,
Modal,
- Alert,
- ScrollView
+ Alert
} from 'react-native';
import { useNavigation, useIsFocused } from '@react-navigation/native';
-import { Calendar } from 'react-native-calendars';
import { Feather } from '@expo/vector-icons';
import { Database } from '../database/Database';
import {
@@ -24,16 +22,16 @@ import {
} from '../services/PrintService';
import { useTheme } from '../context/ThemeContext';
import { getTodayFormatted } from '../utils/dateUtils';
-import { theme } from '../theme';
import StudentStatusModal from '../components/modals/StudentStatusModal';
+import RecordPrintModal from '../components/modals/RecordPrintModal';
import Separator from '../components/Separator';
import ActionMenuItem from '../components/ActionMenuItem';
import FloatingActionButton from '../components/FloatingActionButton';
import StatusBadge from '../components/StatusBadge';
export default function StudentListScreen() {
- // eslint-disable-next-line no-shadow
const { theme } = useTheme();
+ const styles = useMemo(() => createStyles(theme), [theme]);
const navigation = useNavigation();
const isFocused = useIsFocused();
@@ -75,79 +73,8 @@ export default function StudentListScreen() {
return { activeCount: active, pausedCount: paused, totalCount: students.length };
}, [students]);
- // 수업일지 출력 기간 필터 관련 상태
+ // 수업일지 출력 모달용 기록 목록
const [selectedStudentRecords, setSelectedStudentRecords] = useState([]);
- const [periodFilter, setPeriodFilter] = useState('all');
- const [customStartDate, setCustomStartDate] = useState(() => {
- const d = new Date();
- d.setMonth(d.getMonth() - 1);
- const year = d.getFullYear();
- const month = String(d.getMonth() + 1).padStart(2, '0');
- const day = String(d.getDate()).padStart(2, '0');
- return `${year}-${month}-${day}`;
- });
- const [customEndDate, setCustomEndDate] = useState(getTodayFormatted());
- const [activeDatePicker, setActiveDatePicker] = useState(null); // 'start' | 'end' | null
-
- // 필터링된 일지 목록 및 기간명 계산
- const { filteredRecordsForPrint, periodTitle } = useMemo(() => {
- if (!selectedStudentRecords.length) {
- return { filteredRecordsForPrint: [], periodTitle: '전체 기간' };
- }
-
- const today = new Date();
- const todayStr = today.toISOString().split('T')[0];
-
- if (periodFilter === 'custom') {
- const start = customStartDate <= customEndDate ? customStartDate : customEndDate;
- const end = customStartDate <= customEndDate ? customEndDate : customStartDate;
- const filtered = selectedStudentRecords.filter(r => r.class_date >= start && r.class_date <= end);
- return {
- filteredRecordsForPrint: filtered,
- periodTitle: `지정 기간 (${start} ~ ${end})`,
- };
- }
-
- if (periodFilter === '1m') {
- const oneMonthAgo = new Date();
- oneMonthAgo.setMonth(today.getMonth() - 1);
- const limitStr = oneMonthAgo.toISOString().split('T')[0];
- const filtered = selectedStudentRecords.filter(r => r.class_date >= limitStr);
- return {
- filteredRecordsForPrint: filtered,
- periodTitle: `최근 1개월 (${limitStr} ~ ${todayStr})`,
- };
- }
-
- if (periodFilter === '3m') {
- const threeMonthsAgo = new Date();
- threeMonthsAgo.setMonth(today.getMonth() - 3);
- const limitStr = threeMonthsAgo.toISOString().split('T')[0];
- const filtered = selectedStudentRecords.filter(r => r.class_date >= limitStr);
- return {
- filteredRecordsForPrint: filtered,
- periodTitle: `최근 3개월 (${limitStr} ~ ${todayStr})`,
- };
- }
-
- if (periodFilter === 'current_month') {
- const yearMonth = todayStr.substring(0, 7); // YYYY-MM
- const filtered = selectedStudentRecords.filter(r => r.class_date && r.class_date.startsWith(yearMonth));
- return {
- filteredRecordsForPrint: filtered,
- periodTitle: `${today.getFullYear()}년 ${today.getMonth() + 1}월`,
- };
- }
-
- // 기본: 전체 기간
- const sorted = [...selectedStudentRecords].sort((a, b) => a.class_date.localeCompare(b.class_date));
- const start = sorted[0]?.class_date || todayStr;
- const end = sorted[sorted.length - 1]?.class_date || todayStr;
- return {
- filteredRecordsForPrint: selectedStudentRecords,
- periodTitle: `전체 기간 (${start} ~ ${end})`,
- };
- }, [selectedStudentRecords, periodFilter, customStartDate, customEndDate]);
// 학생 클릭 처리: 선택 모달 노출
const handleStudentPress = (student) => {
@@ -181,45 +108,11 @@ export default function StudentListScreen() {
if (!selectedStudent) return;
setActionModalVisible(false);
setPrintType('records');
- setPeriodFilter('all');
- setActiveDatePicker(null);
const recs = await Database.getRecordsByStudent(selectedStudent.id);
setSelectedStudentRecords(recs || []);
setPrintOptionModalVisible(true);
};
- // 인쇄 실행
- const handleExecutePrint = async () => {
- if (!selectedStudent) return;
- setPrintOptionModalVisible(false);
-
- if (printType === 'student') {
- await printStudentProfile(selectedStudent);
- } else {
- if (filteredRecordsForPrint.length === 0) {
- Alert.alert('알림', `${selectedStudent.name} 학생의 해당 기간 수업 일지가 없습니다.`);
- return;
- }
- await printClassRecords(selectedStudent, filteredRecordsForPrint, periodTitle);
- }
- };
-
- // PDF 공유 실행
- const handleExecuteShare = async () => {
- if (!selectedStudent) return;
- setPrintOptionModalVisible(false);
-
- if (printType === 'student') {
- await shareStudentProfile(selectedStudent);
- } else {
- if (filteredRecordsForPrint.length === 0) {
- Alert.alert('알림', `${selectedStudent.name} 학생의 해당 기간 수업 일지가 없습니다.`);
- return;
- }
- await shareClassRecords(selectedStudent, filteredRecordsForPrint, periodTitle);
- }
- };
-
// 학생 목록 가져오기
const loadStudents = async () => {
setLoading(true);
@@ -573,220 +466,35 @@ export default function StudentListScreen() {
onClose={() => setResumeModalVisible(false)}
/>
- {/* 인쇄 및 PDF 공유 방식 선택 모달 */}
- setPrintOptionModalVisible(false)}
- statusBarTranslucent
- >
- setPrintOptionModalVisible(false)}
- >
- e.stopPropagation()}
- >
-
- {selectedStudent && (
- <>
-
-
-
- {printType === 'student' ? '학생 정보 카드 출력' : '수업 일지 보고서 출력'}
-
-
- {printType === 'student'
- ? `${selectedStudent.name} 학생 대상`
- : `${selectedStudent.name} 학생의 수업 기록 (${filteredRecordsForPrint.length}건 선택됨)`}
-
-
- setPrintOptionModalVisible(false)} style={styles.modalCloseBtn}>
-
-
-
-
- {/* 수업일지 보고서 출력인 경우에만 기간 선택 영역 노출 */}
- {printType === 'records' && (
- <>
- 출력 기간 선택
-
- {
- setPeriodFilter('all');
- setActiveDatePicker(null);
- }}
- >
-
- 전체 기간
-
-
-
- {
- setPeriodFilter('current_month');
- setActiveDatePicker(null);
- }}
- >
-
- 이번 달
-
-
-
- {
- setPeriodFilter('1m');
- setActiveDatePicker(null);
- }}
- >
-
- 최근 1개월
-
-
-
- {
- setPeriodFilter('3m');
- setActiveDatePicker(null);
- }}
- >
-
- 최근 3개월
-
-
-
- {
- setPeriodFilter('custom');
- setActiveDatePicker(null);
- }}
- >
-
- 직접 설정
-
-
-
-
- {/* 직접 설정 선택 시 시작일 ~ 종료일 선택기 */}
- {periodFilter === 'custom' && (
-
-
- setActiveDatePicker(activeDatePicker === 'start' ? null : 'start')}
- >
- 시작일
- {customStartDate}
-
-
- ~
-
- setActiveDatePicker(activeDatePicker === 'end' ? null : 'end')}
- >
- 종료일
- {customEndDate}
-
-
-
- {/* 날짜 선택 인라인 캘린더 */}
- {activeDatePicker && (
-
-
-
- {activeDatePicker === 'start' ? '시작 날짜 선택' : '종료 날짜 선택'}
-
-
- {
- if (activeDatePicker === 'start') {
- setCustomStartDate(day.dateString);
- if (day.dateString > customEndDate) {
- setCustomEndDate(day.dateString);
- }
- } else {
- setCustomEndDate(day.dateString);
- if (day.dateString < customStartDate) {
- setCustomStartDate(day.dateString);
- }
- }
- setActiveDatePicker(null);
- }}
- theme={{
- selectedDayBackgroundColor: theme.colors.primary,
- todayTextColor: theme.colors.primary,
- arrowColor: theme.colors.primary,
- }}
- />
-
- )}
-
- )}
-
- {/* 선택된 대상 요약 배너 */}
-
-
-
- 조회: {periodTitle} ({filteredRecordsForPrint.length}회차 기록)
-
-
- >
- )}
-
- {/* 옵션 1: 무선/유선 프린터로 인쇄 */}
-
-
- {/* 옵션 2: PDF 파일 공유 (카톡/메시지) */}
-
- >
- )}
-
-
-
-
+ onClose={() => setPrintOptionModalVisible(false)}
+ printType={printType}
+ student={selectedStudent}
+ records={selectedStudentRecords}
+ onPrintStudent={(stud) => printStudentProfile(stud)}
+ onShareStudent={(stud) => shareStudentProfile(stud)}
+ onPrintRecords={(stud, recs, title) => {
+ if (!recs.length) {
+ Alert.alert('알림', `${stud?.name || '학생'} 학생의 해당 기간 수업 일지가 없습니다.`);
+ return;
+ }
+ printClassRecords(stud, recs, title);
+ }}
+ onShareRecords={(stud, recs, title) => {
+ if (!recs.length) {
+ Alert.alert('알림', `${stud?.name || '학생'} 학생의 해당 기간 수업 일지가 없습니다.`);
+ return;
+ }
+ shareClassRecords(stud, recs, title);
+ }}
+ />
);
}
-const styles = StyleSheet.create({
+const createStyles = (theme) => StyleSheet.create({
container: {
flex: 1,
backgroundColor: theme.colors.surfaceVariant,
@@ -928,129 +636,12 @@ const styles = StyleSheet.create({
modalCloseBtn: {
padding: 4,
},
- filterSectionLabel: {
- fontSize: 13,
- fontWeight: '700',
- color: theme.colors.textPrimary,
- marginBottom: 10,
- },
- filterChipGroup: {
- flexDirection: 'row',
- flexWrap: 'wrap',
- gap: 8,
- marginBottom: 14,
- },
- filterChip: {
- paddingHorizontal: 12,
- paddingVertical: 7,
- borderRadius: 20,
- backgroundColor: theme.colors.surfaceVariant,
- borderWidth: 1,
- borderColor: 'transparent',
- },
- filterChipActive: {
- backgroundColor: '#EFF6FF',
- borderColor: theme.colors.primary,
- },
- filterChipText: {
- fontSize: 13,
- color: theme.colors.textSecondary,
- fontWeight: '600',
- },
- filterChipTextActive: {
- color: theme.colors.primary,
- fontWeight: '700',
- },
- summaryBadgeBox: {
- flexDirection: 'row',
- alignItems: 'center',
- backgroundColor: theme.colors.secondaryContainer,
- paddingHorizontal: 12,
- paddingVertical: 8,
- borderRadius: 8,
- marginBottom: 16,
- },
- summaryBadgeText: {
- fontSize: 12.5,
- color: theme.colors.primary,
- fontWeight: '600',
- },
- customDateContainer: {
- backgroundColor: theme.colors.surfaceVariant + '60',
- padding: 12,
- borderRadius: 12,
- marginBottom: 14,
- borderWidth: 1,
- borderColor: theme.colors.outline + '40',
- },
- customDateRow: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- },
- customDateBtn: {
- flex: 1,
- backgroundColor: theme.colors.white,
- paddingVertical: 8,
- paddingHorizontal: 10,
- borderRadius: 8,
- borderWidth: 1,
- borderColor: theme.colors.outline,
- alignItems: 'center',
- },
- customDateBtnActive: {
- borderColor: theme.colors.primary,
- backgroundColor: '#EFF6FF',
- },
- customDateLabel: {
- fontSize: 11,
- color: theme.colors.textSecondary,
- marginBottom: 2,
- fontWeight: '600',
- },
- customDateText: {
- fontSize: 13,
- fontWeight: '700',
- color: theme.colors.textPrimary,
- },
- customDateTilde: {
- fontSize: 16,
- fontWeight: 'bold',
- color: theme.colors.textSecondary,
- marginHorizontal: 8,
- },
- inlineCustomCalendar: {
- marginTop: 10,
- backgroundColor: theme.colors.white,
- borderRadius: 10,
- padding: 8,
- borderWidth: 1,
- borderColor: theme.colors.outline + '40',
- },
- inlineCustomCalendarHeader: {
- paddingBottom: 6,
- borderBottomWidth: 1,
- borderBottomColor: theme.colors.surfaceVariant,
- marginBottom: 6,
- alignItems: 'center',
- },
- inlineCustomCalendarTitle: {
- fontSize: 12,
- fontWeight: '700',
- color: theme.colors.primary,
- },
searchIcon: {
marginRight: 8,
},
addBtnIcon: {
marginRight: 4,
},
- modalScrollContent: {
- paddingBottom: 10,
- },
- summaryBadgeIcon: {
- marginRight: 6,
- },
tabContainer: {
flexDirection: 'row',
backgroundColor: theme.colors.surfaceVariant + '70',
diff --git a/src/screens/WeeklyPlanScreen.js b/src/screens/WeeklyPlanScreen.js
index 5ea208f..0863b80 100644
--- a/src/screens/WeeklyPlanScreen.js
+++ b/src/screens/WeeklyPlanScreen.js
@@ -8,25 +8,21 @@ import {
ScrollView,
SafeAreaView,
ActivityIndicator,
- Modal,
Alert,
Linking,
Platform,
- KeyboardAvoidingView,
Keyboard,
- useWindowDimensions,
} from 'react-native';
import { Feather, MaterialCommunityIcons } from '@expo/vector-icons';
import { useNavigation, useIsFocused } from '@react-navigation/native';
import { Database } from '../database/Database';
-import { getMondayOfWeek, getDateFromMondayOffset } from '../utils/dateUtils';
+import { getMondayOfWeek, getDateFromMondayOffset, getTodayFormatted } from '../utils/dateUtils';
import { formatPhoneInfo } from '../utils/stringUtils';
import { printWeeklyReport, shareWeeklyReport, shareWeeklyReportDocx, shareWeeklyReportHwpx } from '../services/PrintService';
import { useTheme } from '../context/ThemeContext';
-import { theme } from '../theme';
import CallManageModal from '../components/modals/CallManageModal';
import WeeklyExportModal from '../components/modals/WeeklyExportModal';
-import TimePickerModal from '../components/modals/TimePickerModal';
+import ScheduleEditModal from '../components/modals/ScheduleEditModal';
const DAY_LABELS = ['월', '화', '수', '목', '금', '토', '일'];
@@ -50,8 +46,8 @@ const TIME_SLOTS = [
* @returns {React.JSX.Element} 주간 시간표 화면 요소
*/
export default function WeeklyPlanScreen() {
- // eslint-disable-next-line no-shadow
const { theme } = useTheme();
+ const styles = useMemo(() => createStyles(theme), [theme]);
const navigation = useNavigation();
const isFocused = useIsFocused();
@@ -65,7 +61,11 @@ export default function WeeklyPlanScreen() {
// 뷰 모드: 'table' (주간 전체 타임테이블) | 'timeline' (요일별 상세 타임라인)
const [viewMode, setViewMode] = useState('table');
- const [selectedDayOffset, setSelectedDayOffset] = useState(0); // 0(월) ~ 6(일)
+ const [selectedDayOffset, setSelectedDayOffset] = useState(() => {
+ const d = new Date().getDay();
+ return d === 0 ? 6 : d - 1;
+ }); // 0(월) ~ 6(일)
+ const todayStr = useMemo(() => getTodayFormatted(), []);
// 하단 부가 업무 섹션 접기/펼치기
const [bottomNotesExpanded, setBottomNotesExpanded] = useState(false);
@@ -84,16 +84,10 @@ export default function WeeklyPlanScreen() {
const [formPhoneInfo, setFormPhoneInfo] = useState('');
const [formDayOfWeek, setFormDayOfWeek] = useState(1);
const [formStartTime, setFormStartTime] = useState('10:00');
- const { height: screenHeight } = useWindowDimensions();
const [formStatusNote, setFormStatusNote] = useState('');
- const [studentPickerVisible, setStudentPickerVisible] = useState(false);
- const [studentSearchQuery, setStudentSearchQuery] = useState('');
- const [timePickerVisible, setTimePickerVisible] = useState(false);
const [isKeyboardVisible, setIsKeyboardVisible] = useState(false);
const [keyboardHeight, setKeyboardHeight] = useState(0);
const mainScrollRef = useRef(null);
- const editModalScrollRef = useRef(null);
- const focusedModalFieldRef = useRef(null);
useEffect(() => {
const showSub = Keyboard.addListener(
@@ -103,11 +97,6 @@ export default function WeeklyPlanScreen() {
if (e?.endCoordinates?.height) {
setKeyboardHeight(e.endCoordinates.height);
}
- if (focusedModalFieldRef.current === 'bottom') {
- setTimeout(() => {
- editModalScrollRef.current?.scrollToEnd({ animated: true });
- }, 80);
- }
}
);
const hideSub = Keyboard.addListener(
@@ -115,7 +104,6 @@ export default function WeeklyPlanScreen() {
() => {
setIsKeyboardVisible(false);
setKeyboardHeight(0);
- focusedModalFieldRef.current = null;
}
);
return () => {
@@ -129,14 +117,7 @@ export default function WeeklyPlanScreen() {
isKeyboardVisible && {
paddingBottom: Platform.OS === 'android' ? keyboardHeight + 40 : 120,
},
- ], [isKeyboardVisible, keyboardHeight]);
-
- const editModalScrollContentDynamicStyle = useMemo(() => [
- styles.modalScrollContent,
- isKeyboardVisible && {
- paddingBottom: Platform.OS === 'android' ? Math.max(keyboardHeight * 0.6, 120) : 48,
- },
- ], [isKeyboardVisible, keyboardHeight]);
+ ], [styles.mainScrollContent, isKeyboardVisible, keyboardHeight]);
// 작성된 수업 일지 목록 (실시간 완료 매핑용)
const [allRecords, setAllRecords] = useState([]);
@@ -283,8 +264,6 @@ export default function WeeklyPlanScreen() {
phoneList.push(`(전화)${homePhone}`);
}
setFormPhoneInfo(phoneList.join('\n'));
-
- setStudentPickerVisible(false);
};
// 모달 내 요일 변경 핸들러 (학생이 이미 선택되어 있다면 해당 요일의 정규 수업 일정 정보 자동 연동)
@@ -595,54 +574,36 @@ export default function WeeklyPlanScreen() {
});
};
- // 전화 걸기 헬퍼
+ // 전화 걸기 헬퍼 (다중 연락처 시 선택창 표시)
const handleMakeCall = (phoneStr) => {
if (!phoneStr) return;
- const match = phoneStr.match(/\d{2,3}-\d{3,4}-\d{4}/);
- if (match) {
- Linking.openURL(`tel:${match[0]}`);
- }
- };
+ const matches = phoneStr.match(/(?:\([^)]+\)\s*)?\d{2,3}-\d{3,4}-\d{4}/g);
+ if (!matches || matches.length === 0) return;
- // 필터링된 학생 목록 (학생 선택 모달용 - 가나다 오름차순 정렬)
- const filteredStudents = useMemo(() => {
- const list = [...students].sort((a, b) =>
- (a.name || '').localeCompare(b.name || '', 'ko')
- );
- if (!studentSearchQuery.trim()) return list;
- return list.filter((s) =>
- (s.name || '').toLowerCase().includes(studentSearchQuery.toLowerCase())
- );
- }, [students, studentSearchQuery]);
+ const dialNumber = (raw) => {
+ const numMatch = raw.match(/\d{2,3}-\d{3,4}-\d{4}/);
+ if (numMatch) {
+ Linking.openURL(`tel:${numMatch[0]}`);
+ }
+ };
- // 키보드 상태 및 화면 크기에 따른 학생 선택 모달 동적 스타일 (키보드 가림 방지)
- const studentPickerContainerStyle = useMemo(() => {
- if (!isKeyboardVisible) {
- return [styles.studentPickerOverlay, styles.studentPickerOverlayCentered];
- }
- return [
- styles.studentPickerOverlay,
- styles.studentPickerOverlayKeyboard,
- ];
- }, [isKeyboardVisible]);
-
- const studentPickerContentStyle = useMemo(() => {
- if (!isKeyboardVisible) {
- return [
- styles.pickerModalContent,
- styles.pickerModalContentCentered,
- { maxHeight: Math.min(screenHeight * 0.75, 520) },
- ];
+ if (matches.length === 1) {
+ dialNumber(matches[0]);
+ return;
}
- // 키보드가 올라왔을 때는 화면 상단에 안정적으로 위치하고, 키보드 바로 위까지 최대 높이 확보
- const safeTop = Platform.OS === 'android' ? 36 : 48;
- const availableHeight = screenHeight - keyboardHeight - safeTop - 20;
- return [
- styles.pickerModalContent,
- styles.pickerModalContentKeyboard,
- { maxHeight: Math.max(availableHeight, 240) },
- ];
- }, [isKeyboardVisible, keyboardHeight, screenHeight]);
+
+ Alert.alert(
+ '통화할 번호 선택',
+ '연결할 전화번호를 선택해 주세요.',
+ [
+ ...matches.map((item) => ({
+ text: item.trim(),
+ onPress: () => dialNumber(item),
+ })),
+ { text: '취소', style: 'cancel' },
+ ]
+ );
+ };
// 시간표에는 재원생만 표기: 휴회 중인 학생의 수업은 시간표 렌더링에서 제외
const activeScheduleItems = useMemo(() => {
@@ -798,9 +759,13 @@ export default function WeeklyPlanScreen() {
{[1, 2, 3, 4, 5, 6].map((dayVal) => {
const dStr = getDayDateString(dayVal);
const [, m, d] = dStr.split('-');
+ const isToday = dStr === todayStr;
return (
-
-
+
+
{DAY_LABELS[dayVal - 1]}({parseInt(m, 10)}/{parseInt(d, 10)})
@@ -938,16 +903,33 @@ export default function WeeklyPlanScreen() {
const dStr = getDayDateString(idx + 1);
const [, , d] = dStr.split('-');
const isSelected = selectedDayOffset === idx;
+ const isToday = dStr === todayStr;
return (
setSelectedDayOffset(idx)}
>
-
+
{label}
-
+
{parseInt(d, 10)}일
@@ -1194,339 +1176,32 @@ export default function WeeklyPlanScreen() {
)}
- {/* ======================= 모달 1: 수업 추가 / 편집 모달 ======================= */}
- setEditModalVisible(false)}
- >
-
- {
- if (isKeyboardVisible) {
- Keyboard.dismiss();
- } else {
- setEditModalVisible(false);
- }
- }}
- >
- true}
- >
-
-
- {selectedItem ? '수업 일정 변경 / 메모' : '새 주간 수업 등록'}
-
- setEditModalVisible(false)}>
-
-
-
-
-
- {/* 학생 선택 버튼 */}
-
- 학생 선택 *
- {
- setStudentSearchQuery('');
- setStudentPickerVisible(true);
- }}
- >
-
- {formStudentName || '학생 주소록에서 선택하기'}
-
-
-
-
-
- {/* 요일 및 시간 선택 */}
-
-
- 요일
-
- {DAY_LABELS.map((d, idx) => (
- handleDaySelect(idx + 1)}
- >
-
- {d}
-
-
- ))}
-
-
-
-
- {/* 시작 시간 */}
-
- 시작 시간 *
- setTimePickerVisible(true)}
- >
-
- ⏰ {formStartTime || '10:00'}
-
-
-
-
-
- {/* 과목 */}
-
- 수업 과목 / 과정
-
-
-
- {/* 주소 및 연락처 */}
-
- 방문 주소
- {
- focusedModalFieldRef.current = 'bottom';
- setTimeout(() => {
- editModalScrollRef.current?.scrollTo({ y: 160, animated: true });
- }, 120);
- }}
- placeholder="거주지 주소"
- placeholderTextColor={theme.colors.outline}
- multiline={true}
- />
-
-
-
- 연락처 정보
- {
- focusedModalFieldRef.current = 'bottom';
- setTimeout(() => {
- editModalScrollRef.current?.scrollToEnd({ animated: true });
- }, 120);
- }}
- placeholder="(모)010-0000-0000
(본)010-0000-0000"
- placeholderTextColor={theme.colors.outline}
- multiline={true}
- />
-
-
- {/* 변동 메모 및 프리셋 */}
-
- 이번 주 특이사항 / 변동 메모 (PDF에 인쇄됨)
-
- {['이번주만', '휴일', '아픔', '(30분 수업)'].map((chip) => (
- setFormStatusNote(chip)}
- >
- {chip}
-
- ))}
-
- {
- focusedModalFieldRef.current = 'bottom';
- setTimeout(() => {
- editModalScrollRef.current?.scrollToEnd({ animated: true });
- }, 120);
- }}
- placeholder="예: => 이번주만, => 보강"
- placeholderTextColor={theme.colors.outline}
- />
-
-
-
- {/* 하단 액션 버튼 (항상 화면 하단에 고정 표시) */}
-
- {selectedItem && (
- handleDeleteScheduleItem(selectedItem.id)}
- >
-
- 이번주 제외
-
- )}
-
- {selectedItem && (
- handleGoToClassRecord(selectedItem)}
- >
-
-
- {getMatchedRecord(selectedItem) ? '일지 수정' : '일지 작성'}
-
-
- )}
-
-
-
- 저장하기
-
-
-
-
-
-
-
- {/* ======================= 모달 2: 학생 선택 팝업 ======================= */}
- {
- if (isKeyboardVisible) {
- Keyboard.dismiss();
- } else {
- setStudentPickerVisible(false);
- }
- }}
- statusBarTranslucent
- >
-
- {/* 바깥 배경 터치 시 키보드 닫기 또는 모달 닫기 */}
- {
- if (isKeyboardVisible) {
- Keyboard.dismiss();
- } else {
- setStudentPickerVisible(false);
- }
- }}
- />
-
- true}>
-
-
-
- 학생 선택
-
- {
- Keyboard.dismiss();
- setStudentPickerVisible(false);
- }}
- hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
- >
-
-
-
-
-
-
-
- {studentSearchQuery.length > 0 && (
- setStudentSearchQuery('')}
- hitSlop={{ top: 6, bottom: 6, left: 6, right: 6 }}
- >
-
-
- )}
-
-
-
- {filteredStudents.length === 0 ? (
-
-
-
- {studentSearchQuery ? '일치하는 학생이 없습니다.' : '등록된 학생이 없습니다.'}
-
-
- ) : (
- filteredStudents.map((s) => (
- {
- Keyboard.dismiss();
- handleSelectStudent(s);
- }}
- >
-
- {s.name}
-
- {s.school_grade || '학년 미지정'} | {s.study_method || '방문'}
-
-
-
-
- ))
- )}
-
-
-
-
+ onClose={() => setEditModalVisible(false)}
+ selectedItem={selectedItem}
+ dayLabels={DAY_LABELS}
+ students={students}
+ formStudentName={formStudentName}
+ formDayOfWeek={formDayOfWeek}
+ formStartTime={formStartTime}
+ formSubject={formSubject}
+ formAddress={formAddress}
+ formPhoneInfo={formPhoneInfo}
+ formStatusNote={formStatusNote}
+ onChangeSubject={setFormSubject}
+ onChangeAddress={setFormAddress}
+ onChangePhoneInfo={setFormPhoneInfo}
+ onChangeStatusNote={setFormStatusNote}
+ onChangeStartTime={setFormStartTime}
+ onSelectDay={handleDaySelect}
+ onSelectStudent={handleSelectStudent}
+ onSave={handleSaveScheduleItem}
+ onDelete={() => selectedItem && handleDeleteScheduleItem(selectedItem.id)}
+ matchedRecord={selectedItem ? getMatchedRecord(selectedItem) : null}
+ onGoToClassRecord={() => selectedItem && handleGoToClassRecord(selectedItem)}
+ />
{/* ======================= 모달 3: 전화관리 추가 모달 ======================= */}
shareWeeklyReportHwpx(activeWeeklyPlan)}
/>
- {/* ======================= 모달 5: 시작 시간 GUI 선택 팝업 ======================= */}
- setTimePickerVisible(false)}
- />
);
}
-const styles = StyleSheet.create({
+const createStyles = (theme) => StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F8FAFC',
@@ -1721,7 +1388,7 @@ const styles = StyleSheet.create({
elevation: 2,
},
tableInner: {
- minWidth: 460,
+ minWidth: 500,
},
tableHeaderRow: {
flexDirection: 'row',
@@ -1743,18 +1410,24 @@ const styles = StyleSheet.create({
color: '#334155',
},
dayHeaderCell: {
- width: 68,
+ width: 76,
paddingVertical: 8,
alignItems: 'center',
justifyContent: 'center',
borderLeftWidth: 1,
borderLeftColor: '#CBD5E1',
},
+ dayHeaderCellToday: {
+ backgroundColor: theme.colors.primary,
+ },
dayHeaderLabel: {
fontSize: 11,
fontWeight: '800',
color: '#1E293B',
},
+ dayHeaderLabelToday: {
+ color: theme.colors.onPrimary,
+ },
tableBodyRow: {
flexDirection: 'row',
borderBottomWidth: 1,
@@ -1776,7 +1449,7 @@ const styles = StyleSheet.create({
color: '#475569',
},
scheduleCell: {
- width: 68,
+ width: 76,
minHeight: 46,
borderRightWidth: 1,
borderRightColor: '#E2E8F0',
@@ -1811,16 +1484,16 @@ const styles = StyleSheet.create({
marginBottom: 2,
},
cellItemTitle: {
- fontSize: 9,
+ fontSize: 10.5,
fontWeight: '800',
color: '#1E3A8A',
},
cellItemSubject: {
- fontSize: 8,
+ fontSize: 9.5,
color: '#2563EB',
},
cellItemNote: {
- fontSize: 7.5,
+ fontSize: 9,
color: '#DC2626',
fontWeight: '700',
},
@@ -1934,6 +1607,10 @@ const styles = StyleSheet.create({
borderWidth: 1,
borderColor: theme.colors.outline + '30',
},
+ dayTabPillToday: {
+ backgroundColor: theme.colors.secondaryContainer,
+ borderColor: theme.colors.primary,
+ },
dayTabPillActive: {
backgroundColor: theme.colors.primary,
borderColor: theme.colors.primary,
@@ -1943,6 +1620,9 @@ const styles = StyleSheet.create({
fontWeight: '700',
color: theme.colors.textSecondary,
},
+ dayTabLabelToday: {
+ color: theme.colors.primary,
+ },
dayTabLabelActive: {
color: theme.colors.onPrimary,
},
@@ -2276,327 +1956,6 @@ const styles = StyleSheet.create({
callDeleteBtn: {
padding: 4,
},
- modalOverlay: {
- flex: 1,
- backgroundColor: 'rgba(0, 0, 0, 0.45)',
- justifyContent: 'flex-end',
- },
- modalKeyboardAvoid: {
- flex: 1,
- },
- editModalContent: {
- backgroundColor: theme.colors.white,
- borderTopLeftRadius: 20,
- borderTopRightRadius: 20,
- paddingHorizontal: 20,
- paddingTop: 20,
- paddingBottom: Platform.OS === 'android' ? 56 : 36,
- maxHeight: '88%',
- },
- editModalContentKeyboard: {
- paddingBottom: 16,
- maxHeight: '94%',
- },
- modalHeader: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- marginBottom: 16,
- paddingBottom: 10,
- borderBottomWidth: 1,
- borderBottomColor: theme.colors.surfaceVariant,
- },
- modalTitle: {
- fontSize: 17,
- fontWeight: 'bold',
- color: theme.colors.textPrimary,
- },
- modalSubtitle: {
- fontSize: 12,
- color: theme.colors.textSecondary,
- marginTop: 2,
- },
- modalScroll: {
- flexShrink: 1,
- },
- modalScrollContent: {
- paddingBottom: 10,
- },
- formGroup: {
- marginBottom: 12,
- },
- formRow: {
- flexDirection: 'row',
- gap: 10,
- marginBottom: 12,
- },
- formCol: {
- gap: 4,
- },
- formLabel: {
- fontSize: 12,
- fontWeight: '700',
- color: theme.colors.textSecondary,
- marginBottom: 4,
- },
- reqStar: {
- color: theme.colors.error,
- },
- studentPickerBtn: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- backgroundColor: theme.colors.surfaceVariant + '50',
- borderWidth: 1,
- borderColor: theme.colors.outline + '40',
- borderRadius: 8,
- paddingHorizontal: 12,
- paddingVertical: 10,
- },
- studentPickerText: {
- fontSize: 14,
- fontWeight: '700',
- color: theme.colors.textPrimary,
- },
- studentPickerPlaceholder: {
- fontSize: 13,
- color: theme.colors.outline,
- },
- daySelectRow: {
- flexDirection: 'row',
- gap: 6,
- },
- daySelectPill: {
- width: 38,
- height: 34,
- borderRadius: 6,
- backgroundColor: theme.colors.surfaceVariant + '70',
- alignItems: 'center',
- justifyContent: 'center',
- },
- daySelectPillActive: {
- backgroundColor: theme.colors.primary,
- },
- daySelectPillText: {
- fontSize: 12,
- fontWeight: '700',
- color: theme.colors.textSecondary,
- },
- daySelectPillTextActive: {
- color: theme.colors.onPrimary,
- },
- formInput: {
- backgroundColor: theme.colors.surfaceVariant + '40',
- borderWidth: 1,
- borderColor: theme.colors.outline + '40',
- borderRadius: 8,
- paddingHorizontal: 10,
- paddingVertical: 8,
- fontSize: 13,
- color: theme.colors.textPrimary,
- },
- presetChipRow: {
- flexDirection: 'row',
- flexWrap: 'wrap',
- gap: 6,
- marginBottom: 6,
- },
- presetChip: {
- backgroundColor: '#FEE2E2',
- paddingHorizontal: 8,
- paddingVertical: 4,
- borderRadius: 6,
- },
- presetChipText: {
- fontSize: 10.5,
- fontWeight: '700',
- color: '#B91C1C',
- },
- modalBtnRow: {
- flexDirection: 'row',
- justifyContent: 'flex-end',
- alignItems: 'center',
- gap: 10,
- marginTop: 10,
- paddingTop: 12,
- borderTopWidth: 1,
- borderTopColor: theme.colors.surfaceVariant,
- },
- modalDeleteBtn: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 4,
- paddingHorizontal: 12,
- paddingVertical: 10,
- borderRadius: 8,
- backgroundColor: '#FEE2E2',
- },
- modalDeleteBtnText: {
- fontSize: 12,
- fontWeight: '700',
- color: theme.colors.error,
- },
- modalRecordBtn: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 4,
- paddingHorizontal: 12,
- paddingVertical: 10,
- borderRadius: 8,
- backgroundColor: theme.colors.surfaceVariant,
- },
- modalRecordBtnText: {
- fontSize: 12,
- fontWeight: '700',
- color: theme.colors.primary,
- },
- modalRecordBtnTextDone: {
- color: '#16A34A',
- },
- modalSaveBtn: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 4,
- paddingHorizontal: 18,
- paddingVertical: 10,
- borderRadius: 8,
- backgroundColor: theme.colors.primary,
- },
- modalSaveBtnText: {
- fontSize: 13,
- fontWeight: '700',
- color: theme.colors.onPrimary,
- },
- studentPickerOverlay: {
- flex: 1,
- backgroundColor: 'rgba(0, 0, 0, 0.55)',
- paddingHorizontal: 18,
- },
- studentPickerOverlayCentered: {
- justifyContent: 'center',
- alignItems: 'center',
- },
- studentPickerOverlayKeyboard: {
- justifyContent: 'flex-start',
- paddingTop: Platform.OS === 'android' ? 36 : 48,
- },
- pickerModalContent: {
- width: '100%',
- backgroundColor: theme.colors.white,
- borderRadius: 16,
- padding: 18,
- shadowColor: '#000',
- shadowOffset: { width: 0, height: 4 },
- shadowOpacity: 0.25,
- shadowRadius: 10,
- elevation: 8,
- },
- pickerModalContentCentered: {
- borderRadius: 16,
- },
- pickerModalContentKeyboard: {
- borderRadius: 16,
- },
- studentPickerTitleRow: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 8,
- },
- searchBar: {
- flexDirection: 'row',
- alignItems: 'center',
- backgroundColor: theme.colors.surfaceVariant + '60',
- borderRadius: 8,
- paddingHorizontal: 10,
- marginBottom: 10,
- },
- searchInput: {
- flex: 1,
- paddingVertical: 9,
- paddingHorizontal: 8,
- fontSize: 14,
- color: theme.colors.textPrimary,
- },
- studentListScroll: {
- flexShrink: 1,
- },
- studentListScrollContent: {
- paddingBottom: 8,
- },
- studentPickerEmpty: {
- alignItems: 'center',
- justifyContent: 'center',
- paddingVertical: 32,
- gap: 8,
- },
- studentPickerEmptyText: {
- fontSize: 13,
- color: theme.colors.textSecondary,
- },
- studentItemRow: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- paddingVertical: 12,
- borderBottomWidth: 1,
- borderBottomColor: theme.colors.surfaceVariant,
- },
- studentItemInfo: {
- flex: 1,
- },
- studentItemName: {
- fontSize: 14,
- fontWeight: '700',
- color: theme.colors.textPrimary,
- },
- studentItemSub: {
- fontSize: 11,
- color: theme.colors.textSecondary,
- marginTop: 2,
- },
- callModalContent: {
- backgroundColor: theme.colors.white,
- borderTopLeftRadius: 20,
- borderTopRightRadius: 20,
- padding: 20,
- },
- pdfModalContent: {
- backgroundColor: theme.colors.white,
- borderTopLeftRadius: 24,
- borderTopRightRadius: 24,
- padding: 24,
- paddingBottom: 36,
- },
- actionMenuItem: {
- flexDirection: 'row',
- alignItems: 'center',
- paddingVertical: 14,
- paddingHorizontal: 12,
- borderRadius: theme.roundness,
- backgroundColor: theme.colors.surfaceVariant + '80',
- marginBottom: 12,
- },
- actionIconBadge: {
- width: 44,
- height: 44,
- borderRadius: 22,
- alignItems: 'center',
- justifyContent: 'center',
- marginRight: 14,
- },
- actionMenuTextContainer: {
- flex: 1,
- },
- actionMenuTitle: {
- fontSize: 15,
- fontWeight: '700',
- color: theme.colors.textPrimary,
- marginBottom: 2,
- },
- actionMenuSub: {
- fontSize: 12,
- color: theme.colors.textSecondary,
- },
mapPinIcon: {
marginTop: 2,
},
@@ -2606,18 +1965,6 @@ const styles = StyleSheet.create({
flex1Half: {
flex: 1.5,
},
- multilineInput: {
- height: 50,
- },
- pdfShareIconBadge: {
- backgroundColor: '#E0F2FE',
- },
- docxShareIconBadge: {
- backgroundColor: '#EFF6FF',
- },
- hwpxShareIconBadge: {
- backgroundColor: '#FFF7ED',
- },
cellItemBoxDone: {
backgroundColor: '#DCFCE7',
borderLeftColor: '#16A34A',
@@ -2746,20 +2093,4 @@ const styles = StyleSheet.create({
color: '#D97706',
fontWeight: '700',
},
- timeSelectorBtn: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- backgroundColor: theme.colors.surfaceVariant + '50',
- borderWidth: 1,
- borderColor: theme.colors.outline + '40',
- borderRadius: 8,
- paddingHorizontal: 10,
- paddingVertical: 9,
- },
- timeSelectorBtnText: {
- fontSize: 13.5,
- fontWeight: '700',
- color: theme.colors.textPrimary,
- },
});