diff --git a/apis/MyAccessListApi.js b/apis/MyAccessListApi.js index 9b4ff2a..79d2d81 100644 --- a/apis/MyAccessListApi.js +++ b/apis/MyAccessListApi.js @@ -1,8 +1,8 @@ import axios from './AxiosInstance'; // 출입증 목록 조회 -export const getAccessList = () => { - const response = axios.get('/passes'); +export const getAccessList = async () => { + const response = await axios.get('/passes'); - return response; + return response.data.data; }; diff --git a/app.config.js b/app.config.js index 23490b4..9b9e9d9 100644 --- a/app.config.js +++ b/app.config.js @@ -26,7 +26,7 @@ export default { }, extra: { //BASE_URL: 'http://keywe.site', // EKS 사용시 - BASE_URL: 'http://192.168.0.225:8081', // 도커 사용시 - 본인 pc IPv4 주소로 수정하세용 + BASE_URL: 'http://192.168.0.111:8081', // 도커 사용시 - 본인 pc IPv4 주소로 수정하세용 }, }, }; diff --git a/assets/images/logoGreen.png b/assets/images/logoGreen.png index e75478d..1617d81 100644 Binary files a/assets/images/logoGreen.png and b/assets/images/logoGreen.png differ diff --git a/assets/images/logoIcon.png b/assets/images/logoIcon.png index de5bc7a..9101560 100644 Binary files a/assets/images/logoIcon.png and b/assets/images/logoIcon.png differ diff --git a/assets/images/logoLoading.png b/assets/images/logoLoading.png new file mode 100644 index 0000000..de5bc7a Binary files /dev/null and b/assets/images/logoLoading.png differ diff --git a/assets/images/logoText.png b/assets/images/logoText.png index 31c8cdf..d65b4d4 100644 Binary files a/assets/images/logoText.png and b/assets/images/logoText.png differ diff --git a/assets/images/logoWhite.png b/assets/images/logoWhite.png index cf0cb6b..a1cec9f 100644 Binary files a/assets/images/logoWhite.png and b/assets/images/logoWhite.png differ diff --git a/components/cards/Dot.js b/components/cards/Dot.js index ede8791..38e631f 100644 --- a/components/cards/Dot.js +++ b/components/cards/Dot.js @@ -15,7 +15,7 @@ const Dot = ({ active }) => { useEffect(() => { Animated.timing(animatedWidth, { toValue: active ? DOT_ACTIVE_WIDTH : DOT_WIDTH, - duration: 300, + duration: 200, useNativeDriver: false, }).start(); }, [active, animatedWidth]); diff --git a/components/cards/DotPagination.js b/components/cards/DotPagination.js new file mode 100644 index 0000000..4bcfacd --- /dev/null +++ b/components/cards/DotPagination.js @@ -0,0 +1,81 @@ +import React, { useMemo, useEffect, useRef } from 'react'; +import { View, TouchableOpacity, Text, Animated } from 'react-native'; +import Dot from './Dot'; +import { styles } from './styles/DotPagination.styles'; + +// 한번에 보이는 최대 DOT 개수 +const MAX_DOTS = 8; + +const DotPagination = ({ total, currentIndex, onPress }) => { + const groupCount = Math.ceil(total / MAX_DOTS); //전체 도트 수를 MAX_DOTS로 나눈 그룹 개수 + const currentGroup = Math.floor(currentIndex / MAX_DOTS); //현재 인덱스가 속한 그룹 번호 + const start = currentGroup * MAX_DOTS; // 현재 그룹에서 첫번째 도트 인덱스 + const end = Math.min(start + MAX_DOTS, total); // 현재 그룹에서 마지막 도트 다음 인덱스 + // 현재 그룹에서 보여줄 도트 인덱스 + // useMemo를 통해 최적화(start, end가 변하지 않으면 이전에 만든 배열을 재사용) + const dotIndexes = useMemo( + () => Array.from({ length: end - start }, (_, i) => start + i), // 각 요소의 인덱스 i에 start를 더함 + [start, end], + ); + + // 왼쪽/오른쪽 화살표의 애니메이션 위치값 (초기값: 첫그룹이면 왼쪽은 숨김, 마지막그룹이면 오른쪽은 숨김) + const leftArrowTranslate = useRef(new Animated.Value(currentGroup > 0 ? 0 : -30)).current; + const rightArrowTranslate = useRef( + new Animated.Value(currentGroup < groupCount - 1 ? 0 : 30), + ).current; + + // 그룹이 바뀔 때마다 왼쪽 화살표 애니메이션 + useEffect(() => { + Animated.timing(leftArrowTranslate, { + toValue: currentGroup > 0 ? 0 : -30, // 첫그룹이면 숨김(-30), 아니면 보임(0) + duration: 250, + useNativeDriver: true, + }).start(); + }, [currentGroup, leftArrowTranslate]); + + // 그룹이 바뀔 때마다 오른쪽 화살표 애니메이션 + useEffect(() => { + Animated.timing(rightArrowTranslate, { + toValue: currentGroup < groupCount - 1 ? 0 : 30, // 마지막그룹이면 숨김(30), 아니면 보임(0) + duration: 250, + useNativeDriver: true, + }).start(); + }, [currentGroup, rightArrowTranslate, groupCount]); + + // 왼쪽 화살표 클릭: 첫 그룹으로 이동 + const goToFirstGroup = () => onPress(0); + // 오른쪽 화살표 클릭: 마지막 그룹의 첫 도트로 이동 + const goToLastGroup = () => onPress((groupCount - 1) * MAX_DOTS); + + return ( + + + + {currentGroup > 0 && ( + + {'<'} + + )} + + + + {dotIndexes.map((idx) => ( + onPress(idx)}> + + + ))} + + + + {currentGroup < groupCount - 1 && ( + + {'>'} + + )} + + + + ); +}; + +export default DotPagination; diff --git a/components/cards/QrCard.js b/components/cards/QrCard.js index 3ee8a62..b99ce3d 100644 --- a/components/cards/QrCard.js +++ b/components/cards/QrCard.js @@ -9,7 +9,7 @@ import { colors } from '../../constants/colors'; import { hospitalName } from '../../mocks/hospitalData'; // hasAccessAuthority: 출입 권한 여부, userVC : VC에 담을 사용자 정보, qrData : QR에 담을 JSON 문자열 -const QrCard = ({ hasAccessAuthority, did, userName, hospitalName }) => { +const QrCard = ({ hasAccessAuthority, did, userName, hospitalName, startDate, expireDate }) => { // 해당 QR의 상세 페이지로 이동 (아직 미구현) //const navigation = useNavigation(); // const navigateToAccessListDeatail = () => { @@ -17,7 +17,7 @@ const QrCard = ({ hasAccessAuthority, did, userName, hospitalName }) => { // }; // 임시: QR에 담을 JSON 문자열 - const qrData = JSON.stringify({ did, userName, hospitalName }); + const qrData = JSON.stringify({ did, userName, hospitalName, startDate, expireDate }); return ( @@ -32,6 +32,8 @@ const QrCard = ({ hasAccessAuthority, did, userName, hospitalName }) => { {userName} {hospitalName} + 시작일: {startDate} + 만료일: {expireDate} ) : ( <> diff --git a/components/cards/QrCards.js b/components/cards/QrCards.js index 5dfc9b1..2aeb5b8 100644 --- a/components/cards/QrCards.js +++ b/components/cards/QrCards.js @@ -2,12 +2,11 @@ import React, { useRef, useState } from 'react'; import { FlatList, View, Dimensions, TouchableOpacity } from 'react-native'; import QrCard from './QrCard'; import styles from './styles/QrCards.styles'; -import Dot from './Dot'; +import DotPagination from './DotPagination'; // 화면의 가로 길이 가져오기 const { width } = Dimensions.get('window'); const CARD_WIDTH = width; -const SIDE_PADDING = (width - CARD_WIDTH) / 2; // 좌우 패딩 계산 const OVERLAP = 80; // 카드가 겹치는 정도 const QrCards = ({ userVC, hasAccessAuthority }) => { @@ -32,7 +31,8 @@ const QrCards = ({ userVC, hasAccessAuthority }) => { // dot(인디케이터) 클릭 시 해당 카드로 이동 const handleDotPress = (index) => { - flatListRef.current?.scrollToIndex({ index, animated: true }); + const offset = index * (CARD_WIDTH - OVERLAP); + flatListRef.current?.scrollToOffset({ offset, animated: true }); }; // 카드 리스트 @@ -47,9 +47,11 @@ const QrCards = ({ userVC, hasAccessAuthority }) => { pagingEnabled={false} // snapToInterval을 사용하므로 false snapToInterval={CARD_WIDTH - OVERLAP} // 카드 단위로 스냅 decelerationRate="fast" // 빠른 스냅 효과 - contentContainerStyle={{ - paddingHorizontal: SIDE_PADDING, // 양쪽에 패딩 추가로 옆 카드 살짝 보이게 - }} + getItemLayout={(data, index) => ({ + length: CARD_WIDTH - OVERLAP, + offset: (CARD_WIDTH - OVERLAP) * index, + index, + })} renderItem={({ item, index }) => ( { did={item.did} userName={item.userName} hospitalName={item.hospitalName} + startDate={item.startDate} + expireDate={item.expireDate} hasAccessAuthority={true} /> @@ -70,11 +74,7 @@ const QrCards = ({ userVC, hasAccessAuthority }) => { onMomentumScrollEnd={onMomentumScrollEnd} /> - {userVC.map((_, i) => ( - handleDotPress(i)}> - - - ))} + ); diff --git a/components/cards/styles/DotPagination.styles.js b/components/cards/styles/DotPagination.styles.js new file mode 100644 index 0000000..59032b7 --- /dev/null +++ b/components/cards/styles/DotPagination.styles.js @@ -0,0 +1,37 @@ +import { StyleSheet } from 'react-native'; +import { colors } from '../../../constants/colors'; +// TODO: 도트 상수 문서화 필요할지도.. 통일 +export const DOT_SIZE = 12; // 도트 크기 +export const DOT_MARGIN = 8; // 도트 사이 간격 +export const MAX_DOTS = 8; // 한 그룹 최대 도트 수 + +export const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'center', + alignSelf: 'center', + marginVertical: 16, + }, + arrowContainer: { + width: 40, + alignItems: 'center', + justifyContent: 'center', + }, + arrow: { + paddingHorizontal: 8, + justifyContent: 'center', + alignItems: 'center', + }, + arrowText: { + fontSize: 24, + fontWeight: 'bold', + color: colors.darkGray, + }, + dotsWrapper: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + flex: 0, + minWidth: DOT_SIZE * MAX_DOTS + DOT_MARGIN * (MAX_DOTS - 1), + }, +}); diff --git a/components/cards/styles/QrCard.styles.js b/components/cards/styles/QrCard.styles.js index 7f13ae1..83a5187 100644 --- a/components/cards/styles/QrCard.styles.js +++ b/components/cards/styles/QrCard.styles.js @@ -11,7 +11,7 @@ export const styles = StyleSheet.create({ elevation: 7, // Android용 borderRadius: 15, width: '70%', - height: '80%', + height: '85%', //TODO : 기존에는 80이었는데, 추후 카드 높이 변경 필요할듯함 marginVertical: '10%', }, cardContainer: { diff --git a/components/lists/NormalListDeep.js b/components/lists/NormalListDeep.js index f0ba30e..17ea908 100644 --- a/components/lists/NormalListDeep.js +++ b/components/lists/NormalListDeep.js @@ -5,16 +5,16 @@ import { styles } from './styles/NormalListDeep.styles'; //노말리스트와 // TODO: Pass-Service 구현 완료 시, 실제 데이터로 변경 필요 //리스트안에 노말리스트가 있는 컴포넌트 -const NormalListDeep = ({ sections = [], onItemPress, renderItem }) => { +const NormalListDeep = ({ sections = [], onItemPress, renderItem, cardStyle }) => { return ( {sections.map((section, idx) => ( - //console.log('Section', section), {section.contentTitle} { if (onItemPress) onItemPress(section, item, index); diff --git a/components/lists/styles/NormalListDeep.styles.js b/components/lists/styles/NormalListDeep.styles.js index 78c9380..cde9716 100644 --- a/components/lists/styles/NormalListDeep.styles.js +++ b/components/lists/styles/NormalListDeep.styles.js @@ -8,7 +8,6 @@ export const styles = StyleSheet.create({ contentContainer: { paddingBottom: 60 }, // 각 아이템 박스 스타일 itemBox: { - marginBottom: 10, marginBottom: -40, }, // 각 아이템 텍스트 스타일 diff --git a/components/loadings/KiwiSpinner.js b/components/loadings/KiwiSpinner.js index fcb6d84..b63a307 100644 --- a/components/loadings/KiwiSpinner.js +++ b/components/loadings/KiwiSpinner.js @@ -27,7 +27,7 @@ const KiwiSpinner = () => { return ( diff --git a/constants/colors.js b/constants/colors.js index de22e3a..f301457 100644 --- a/constants/colors.js +++ b/constants/colors.js @@ -2,6 +2,8 @@ export const colors = { background: '#F0F0F0', white: '#FFFFFF', black: '#464646', + moreLightGreen: '#E7EDE8', + moreLightGray: '#DFDFDF', lightGray: '#B7B7B7', darkGray: '#7E7E7E', primary: '#24562B', // 메인 포인트 색상 diff --git a/modals/MyAccessDetailModal.js b/modals/MyAccessDetailModal.js index 5dd83b3..97385dd 100644 --- a/modals/MyAccessDetailModal.js +++ b/modals/MyAccessDetailModal.js @@ -14,14 +14,15 @@ const MyAccessDetailModal = ({ isVisible, onClose, onConfirm, data }) => { {data.area} 방문자: {data.visitorType} + 시작일: {data.startDate} 만료일: {data.expireDate} 승인 여부: {data.approval} 환자 번호: {data.patientNumber} 내 보호자 - {`김OO\t|\t010-0000-0000`} - {`김OO\t|\t010-0000-0000`} + {`김지수\t|\t010-0000-0000`} + {`손민지\t|\t010-1111-1111`} diff --git a/modals/styles/MyAccessDetailModal.styles.js b/modals/styles/MyAccessDetailModal.styles.js index 9dd394b..8a457cc 100644 --- a/modals/styles/MyAccessDetailModal.styles.js +++ b/modals/styles/MyAccessDetailModal.styles.js @@ -13,14 +13,14 @@ export const styles = StyleSheet.create({ alignSelf: 'center', }, modalTitle: { - fontSize: 25, + fontSize: 22, fontWeight: '600', color: colors.black, marginTop: '7%', alignSelf: 'center', }, modalContentTitle: { - fontSize: 20, + fontSize: 17, fontWeight: '500', color: colors.black, marginTop: '5%', @@ -31,10 +31,10 @@ export const styles = StyleSheet.create({ marginBottom: '7%', }, modalText: { - fontSize: 18, + fontSize: 17, fontWeight: '500', color: colors.darkGray, - marginTop: 10, + marginTop: 2, lineHeight: 30, }, buttonRow: { diff --git a/pages/MainPage.js b/pages/MainPage.js index 967dbe3..dd811f8 100644 --- a/pages/MainPage.js +++ b/pages/MainPage.js @@ -10,28 +10,170 @@ import QrCards from '../components/cards/QrCards'; const MainPage = () => { // 임시: 상태변수로 출입 권한 제어 const [hasAccessAuthority, setHasAccessAuthority] = useState(true); - - // 임시: VC에 담을 사용자 정보 const userVC = [ { - did: 'did:example:123456789abcdefghi', - userName: '김짱구', - hospitalName: '짱구병원', + did: 'did:example:123456789abcdefg01', + userName: '김엘지', + hospitalName: '강북삼성병원', + startDate: '2025-05-17T06:35:05', + expireDate: '2025-05-18T06:33:09', + issuedAt: Date.now(), + }, + { + did: 'did:example:123456789abcdefg02', + userName: '김엘지', + hospitalName: '강북삼성병원', + startDate: '2025-05-15T08:06:27', + expireDate: '2025-05-17T08:06:00', + issuedAt: Date.now(), + }, + { + did: 'did:example:123456789abcdefg03', + userName: '김엘지', + hospitalName: '건국대학교병원', + startDate: '2025-05-15T08:08:15', + expireDate: '2025-05-17T08:06:54', issuedAt: Date.now(), }, { - did: 'did:example:123456789abcdefdhi', - userName: '김짱구', - hospitalName: '흰둥이병원', + did: 'did:example:123456789abcdefg04', + userName: '김엘지', + hospitalName: '건국대학교병원', + startDate: '2025-05-15T08:08:15', + expireDate: '2025-05-17T08:06:54', issuedAt: Date.now(), }, { - did: 'did:example:123456789abcdeffhi', - userName: '김짱구', - hospitalName: '오수병원', + did: 'did:example:123456789abcdefg05', + userName: '김엘지', + hospitalName: '건국대학교병원', + startDate: '2025-05-15T08:08:15', + expireDate: '2025-05-17T08:06:54', issuedAt: Date.now(), }, + { + did: 'did:example:123456789abcdefg06', + userName: '김엘지', + hospitalName: '건국대학교병원', + startDate: '2025-05-15T08:08:15', + expireDate: '2025-05-17T08:06:54', + issuedAt: Date.now(), + }, + { + did: 'did:example:123456789abcdefg07', + userName: '김엘지', + hospitalName: '건국대학교병원', + startDate: '2025-05-15T08:08:15', + expireDate: '2025-05-17T08:06:54', + issuedAt: Date.now(), + }, + { + did: 'did:example:123456789abcdefg08', + userName: '김엘지', + hospitalName: '건국대학교병원', + startDate: '2025-05-15T08:08:15', + expireDate: '2025-05-17T08:06:54', + issuedAt: Date.now(), + }, + { + did: 'did:example:123456789abcdefg09', + userName: '김엘지', + hospitalName: '건국대학교병원', + startDate: '2025-05-15T08:08:15', + expireDate: '2025-05-17T08:06:54', + issuedAt: Date.now(), + }, + { + did: 'did:example:123456789abcdefg10', + userName: '김엘지', + hospitalName: '건국대학교병원', + startDate: '2025-05-15T08:08:15', + expireDate: '2025-05-17T08:06:54', + issuedAt: Date.now(), + }, + { + did: 'did:example:123456789abcdefg11', + userName: '김엘지', + hospitalName: '건국대학교병원', + startDate: '2025-05-15T08:08:15', + expireDate: '2025-05-17T08:06:54', + issuedAt: Date.now(), + }, + { + did: 'did:example:123456789abcdefg12', + userName: '김엘지', + hospitalName: '건국대학교병원', + startDate: '2025-05-15T08:08:15', + expireDate: '2025-05-17T08:06:54', + issuedAt: Date.now(), + }, + { + did: 'did:example:123456789abcdefg13', + userName: '김엘지', + hospitalName: '건국대학교병원', + startDate: '2025-05-15T08:08:15', + expireDate: '2025-05-17T08:06:54', + issuedAt: Date.now(), + }, + { + did: 'did:example:123456789abcdefg14', + userName: '김엘지', + hospitalName: '건국대학교병원', + startDate: '2025-05-15T08:08:15', + expireDate: '2025-05-17T08:06:54', + issuedAt: Date.now(), + }, + { + did: 'did:example:123456789abcdefg15', + userName: '김엘지', + hospitalName: '건국대학교병원', + startDate: '2025-05-15T08:08:15', + expireDate: '2025-05-17T08:06:54', + issuedAt: Date.now(), + }, + { + did: 'did:example:123456789abcdefg16', + userName: '김엘지', + hospitalName: '건국대학교병원', + startDate: '2025-05-15T08:08:15', + expireDate: '2025-05-17T08:06:54', + issuedAt: Date.now(), + }, + { + did: 'did:example:123456789abcdefg17', + userName: '김엘지', + hospitalName: '건국대학교병원', + startDate: '2025-05-15T08:08:15', + expireDate: '2025-05-17T08:06:54', + issuedAt: Date.now(), + }, + { + did: 'did:example:123456789abcdefg18', + userName: '김엘지', + hospitalName: '건국대학교병원', + startDate: '2025-05-15T08:08:15', + expireDate: '2025-05-17T08:06:54', + issuedAt: Date.now(), + }, + { + did: 'did:example:123456789abcdefg19', + userName: '김엘지', + hospitalName: '건국대학교병원', + startDate: '2025-05-15T08:08:15', + expireDate: '2025-05-17T08:06:54', + issuedAt: Date.now(), + }, + { + did: 'did:example:123456789abcdefg20', + userName: '김엘지', + hospitalName: '건국대학교병원', + startDate: '2025-05-15T08:08:15', + expireDate: '2025-05-17T08:06:54', + issuedAt: Date.now(), + }, + // ... (이런 식으로 계속 01, 02, 03 ... 40까지 did 값을 고유하게 부여) ]; + // 임시: QR에 담을 JSON 문자열 //const qrData = JSON.stringify(userVC); diff --git a/pages/MyAccessListPage.js b/pages/MyAccessListPage.js index 2cbb139..e89787a 100644 --- a/pages/MyAccessListPage.js +++ b/pages/MyAccessListPage.js @@ -1,84 +1,243 @@ import React, { useEffect, useState } from 'react'; import { View, Text } from 'react-native'; import NormalListDeep from '../components/lists/NormalListDeep'; -import { MyAccessList } from '../mocks/MyAccessListSample'; //예시 데이터 import { styles } from './styles/MyAccessListPage.styles'; import { getAccessList } from '../apis/MyAccessListApi'; +import { getHospitalList } from '../apis/AccessRequestApi'; import MyAccessDetailModal from '../modals/MyAccessDetailModal'; import { useAuthStore } from '../stores/authStore'; +// 더미 데이터 (로컬 테스트 용) +const mockAccessList = [ + { + passId: 4, + memberId: 1, + hospitalId: 1, + accessAreaNames: [ + '본관 5층 내과병동 501호', + '본관 5층 내과병동 502호', + '본관 5층 내과병동 503호', + ], + visitCategory: 'PATIENT', + patientId: 9, + startedAt: '2025-05-16T06:35:05', + expiredAt: '2025-05-18T06:33:09', + }, + { + passId: 5, + memberId: 1, + hospitalId: 1, + accessAreaNames: ['신관 3층 외과병동 301호', '신관 4층 외과병동 402호'], + visitCategory: 'PATIENT', + patientId: 9, + startedAt: '2025-05-15T08:06:27', + expiredAt: '2025-05-17T08:06:00', + }, + { + passId: 6, + memberId: 1, + hospitalId: 2, + accessAreaNames: ['암센터 7층 항암치료실 701호'], + visitCategory: 'GUARDIAN', + patientId: 10, + startedAt: '2025-05-15T08:08:15', + expiredAt: '2025-05-17T08:06:54', + }, + { + passId: 7, + memberId: 1, + hospitalId: 3, + accessAreaNames: ['본관 3층 내과병동 305호', '본관 4층 외과병동 410호'], + visitCategory: 'GUARDIAN', + patientId: 10, + startedAt: '2025-05-17T08:08:15', + expiredAt: '2025-05-18T08:06:54', + }, + // { + // passId: 8, + // memberId: 1, + // hospitalId: 3, + // accessAreaNames: ['소망관 3층 응급의학과 외상전용 수술실 320호'], + // visitCategory: 'GUARDIAN', + // patientId: 10, + // startedAt: '2025-05-15T08:08:15', + // expiredAt: '2025-06-16T08:06:54', + // }, +]; + const MyAccessListPage = () => { const { setLoading } = useAuthStore(); const [myAccessList, setMyAccessList] = useState([]); + const [hospitalNameList, setHospitalNameList] = useState([]); // Alert 관리 상태변수 const [showModal, setShowModal] = useState(false); // 모달 표시 여부 const [selectedAccess, setSelectedAccess] = useState(null); // 클릭된 출입증 - // 출입증 목록 불러오기 + // 병원 목록 불러오기 useEffect(() => { - const getMyAccessList = async () => { + const getHospitalsName = async () => { setLoading(true); try { - const data = await getAccessList(); - console.log(data.data.data); - setMyAccessList(data.data.data); + const data = await getHospitalList(); + setHospitalNameList(data); // [{ hospitalId, hospitalName }] } catch (error) { - console.error('출입증 목록 불러오기 실패: ', error); + console.error('병원 목록 불러오기 실패:', error); } finally { setLoading(false); } }; - getMyAccessList(); + getHospitalsName(); + }, [setLoading]); + + // 출입증 목록 불러오기 + // useEffect(() => { + // const getMyAccessList = async () => { + // setLoading(true); + // try { + // const data = await getAccessList(); + // console.log(data); + // setMyAccessList(data); + // } catch (error) { + // console.error('출입증 목록 불러오기 실패: ', error); + // } finally { + // setLoading(false); + // } + // }; + // getMyAccessList(); + // }, [setLoading]); + + // 임시 데이터 적용 + useEffect(() => { + setMyAccessList(mockAccessList); // 위에서 만든 mock 데이터로 대체 }, []); // 출입 권한 클릭 시 모달 띄우기 - // TODO: Pass-Service 구현 완료 시, 실제 데이터로 변경 필요 const handleItemPress = (section, item, index) => { const access = item.data; setSelectedAccess({ hospitalName: section.contentTitle, - area: `${access.building_name} ${access.area_name}`, - visitorType: access.visitor_category, - expireDate: access.validate_to, - approval: access.expired ? '출입 대기' : '유효', - patientNumber: access.PatientID, - issuer: access.requester_category, + // area: (access.accessAreaNames || []).join(',\n'), + area: (access.accessAreaNames || []).map((name) => `${name}`).join('\n'), + visitorType: getVisitCategoryLabel(access.visitCategory), + startDate: formatDateTime(access.startedAt), + expireDate: formatDateTime(access.expiredAt), + approval: getApprovalStatus(access.startedAt, access.expiredAt), + patientNumber: access.patientId, + issuer: access.memberId, }); setShowModal(true); }; + // visitCategory 변환 함수 + const getVisitCategoryLabel = (category) => { + switch (category) { + case 'PATIENT': + return '환자'; + case 'GUARDIAN': + return '보호자'; + default: + return category; // 혹시 모르는 값은 그대로 표기 + } + }; + + // 날짜 포맷 함수 (YYYY-MM-DD HH:mm) + const formatDateTime = (date) => { + if (!date) return ''; + const d = new Date(date); + const yyyy = d.getFullYear(); + const mm = String(d.getMonth() + 1).padStart(2, '0'); + const dd = String(d.getDate()).padStart(2, '0'); + const hh = String(d.getHours()).padStart(2, '0'); + const min = String(d.getMinutes()).padStart(2, '0'); + return `${yyyy}-${mm}-${dd} ${hh}:${min}`; + }; + + // 출입 가능 상태 함수 + const getApprovalStatus = (startedAt, expiredAt) => { + const now = new Date(); + const start = new Date(startedAt); + const end = new Date(expiredAt); + + if (start > now) { + return '출입 대기'; + } else if (end < now) { + return '만료'; + } else { + return '출입 가능'; + } + }; + + //병원 Id로 병원 이름 찾기 + const getHospitalName = (hospitalId) => { + const hospital = hospitalNameList.find((hospital) => hospital.hospitalId === hospitalId); + // 병원 목록에 없으면 병원 #id로 표시 + return hospital ? hospital.hospitalName : `병원명 로딩 중 . . .병원: #${hospitalId}`; + }; + // NormalListDeep에 넘길 데이터 가공 - const sections = myAccessList.map((section) => ({ - contentTitle: section.hospital_name, - accessList: section.accessList, - })); + const sections = myAccessList.reduce((acc, cur) => { + //acc - accumulator(누적값, 병원별로 묶안 배열), cur - current(현재 배열에서 처리중인 값) + const hospitalId = cur.hospitalId; + let hospitalName = getHospitalName(hospitalId); //id로 이름 찾아서 저장 + let section = acc.find((sec) => sec.hospitalId === hospitalId); //현재 hospitalId와 같은 section이 있는지 찾는다. + if (!section) { + //섹션이 없으면 새로운 섹션 객체를 만들어 acc에 추가한다. + section = { + hospitalId, + contentTitle: hospitalName, + accessList: [], + }; + acc.push(section); + } + section.accessList.push(cur); //해당 병원 그룹의 accessList 배열에 현재 출입증 추가 + return acc; //누적값 반환해서 다음 루프에 이어감 + }, []); return ( <> - {myAccessList.length > 0 ? ( + {sections.length > 0 ? ( ({ + ...section, + accessList: section.accessList.map((item) => ({ data: item })), + }))} onItemPress={handleItemPress} - renderItem={(item, idx, selected) => ( - - - - {item.data.building_name} {item.data.area_name} - {item.data.visitor_category} - + renderItem={(itemObj, idx, selected) => { + const item = itemObj.data; + return ( + + + + + {'[ ' + getVisitCategoryLabel(item.visitCategory) + ' ]'} + + {(item.accessAreaNames || []).map((area, idx) => ( + + {area} + + ))} + + + + {getApprovalStatus(item.startedAt, item.expiredAt)} + + + - {'\n'}({item.data.validate_to}까지) - - - - - {'\n'} {item.data.expired ? '출입 대기' : '유효'} + 시작일: {formatDateTime(item.startedAt)} + {'\n'}만료일: {formatDateTime(item.expiredAt)} + {'\n'} - - )} + ); + }} /> ) : ( 유효한 출입증이 존재하지 않습니다. diff --git a/pages/styles/MyAccessListPage.styles.js b/pages/styles/MyAccessListPage.styles.js index 83a9ab7..8be0d6b 100644 --- a/pages/styles/MyAccessListPage.styles.js +++ b/pages/styles/MyAccessListPage.styles.js @@ -3,27 +3,62 @@ import { colors } from '../../constants/colors'; export const styles = StyleSheet.create({ container: { + justifyContent: 'space-between', + borderRadius: 5, + borderWidth: 1, + borderColor: colors.moreLightGray, + }, + // 권한 구역 + 출입 가능 여부 (패딩) + infoTextPadding: { + padding: '5%', flexDirection: 'row', justifyContent: 'space-between', + borderBottomWidth: 1, + borderColor: colors.moreLightGray, + backgroundColor: colors.moreLightGreen, }, + // 권한 구역 (패딩) + areaTextPadding: { + width: '75%', + }, + // 권한 구역 - 환자, 보호자 여부 textTitle: { + width: '70%', fontSize: 20, fontWeight: 600, color: colors.black, + lineHeight: 30, }, - text: { - fontSize: 19, + // 권한 구역 - 구역 이름 + areaText: { + fontSize: 18, color: colors.black, + lineHeight: 28, + marginVertical: 2, }, + // 출입 가능 여부 (패딩) + validateTextPadding: { + maxWidth: '20%', + alignSelf: 'center', + marginLeft: 'auto', + }, + // 출입 가능 여부 글자 validateText: { fontSize: 20, fontWeight: 600, color: colors.tertiary, }, + // 출입증 존재하지 않을 시 텍스트 infoText: { marginTop: '10%', fontSize: 20, textAlign: 'center', color: colors.darkGray, }, + // 시작일, 만료일 정보 + text: { + padding: '5%', + fontSize: 15, + color: colors.black, + }, });