From 83f196b8a8b64a257a20724cfd378ff2a5609d28 Mon Sep 17 00:00:00 2001 From: chunjaemin Date: Sun, 15 Feb 2026 22:12:15 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[FEAT/#6]=20shared/ui/select=20=EA=B3=B5?= =?UTF-8?q?=EC=9A=A9=20=EB=93=9C=EB=A1=AD=EB=8B=A4=EC=9A=B4=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B8=B0=EB=8A=A5=20=EC=99=84?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + src/shared/ui/index.ts | 2 + src/shared/ui/select/Select.tsx | 135 ++++++++++++++++++++++++++++++++ src/shared/ui/select/index.ts | 3 + src/shared/ui/select/types.ts | 40 ++++++++++ yarn.lock | 12 +++ 6 files changed, 193 insertions(+) create mode 100644 src/shared/ui/index.ts create mode 100644 src/shared/ui/select/Select.tsx create mode 100644 src/shared/ui/select/index.ts create mode 100644 src/shared/ui/select/types.ts diff --git a/package.json b/package.json index 8e97b55..62807d4 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "react": "19.1.0", "react-dom": "19.1.0", "react-native": "0.81.5", + "react-native-element-dropdown": "^2.12.4", "react-native-gesture-handler": "~2.28.0", "react-native-reanimated": "~4.1.1", "react-native-safe-area-context": "~5.6.0", diff --git a/src/shared/ui/index.ts b/src/shared/ui/index.ts new file mode 100644 index 0000000..e4c0223 --- /dev/null +++ b/src/shared/ui/index.ts @@ -0,0 +1,2 @@ +export * from "./select"; + diff --git a/src/shared/ui/select/Select.tsx b/src/shared/ui/select/Select.tsx new file mode 100644 index 0000000..0f9d85b --- /dev/null +++ b/src/shared/ui/select/Select.tsx @@ -0,0 +1,135 @@ +import { useMemo } from "react"; +import { Text, View } from "react-native"; +import { Dropdown } from "react-native-element-dropdown"; + +import type { SelectItem, SelectProps } from "./types"; + +const SIZES = { + sm: { + fieldPx: 12, + fieldPy: 10, + fontSize: 14, + }, + md: { + fieldPx: 12, + fieldPy: 12, + fontSize: 16, + }, +} as const; + +export function Select({ + items, + value, + onChange, + placeholder = "선택", + disabled = false, + label, + helperText, + errorText, + size = "md", + testID, +}: SelectProps) { + const hasError = Boolean(errorText); + const sizeToken = SIZES[size]; + + const dropdownData = useMemo(() => { + // react-native-element-dropdown은 disabled key가 없어서 + // item 렌더링/선택 로직에서 직접 처리한다. + return items; + }, [items]); + + const selectedItem: SelectItem | null = useMemo(() => { + if (value == null) return null; + return items.find((it) => it.value === value) ?? null; + }, [items, value]); + + return ( + + {!!label && ( + + {label} + + )} + + + { + // disabled 항목은 선택 무시 + if (item?.disabled) return; + onChange(item?.value ?? null); + }} + style={{ + backgroundColor: "transparent", + }} + containerStyle={{ + borderRadius: 12, + overflow: "hidden", + }} + itemContainerStyle={{ + paddingHorizontal: 12, + paddingVertical: 12, + opacity: 1, + }} + activeColor="#E5F6FE" + placeholderStyle={{ + fontFamily: "Pretendard-Regular", + fontSize: sizeToken.fontSize, + color: "#8E9398", + }} + selectedTextStyle={{ + fontFamily: selectedItem?.value ? "Pretendard-Regular" : "Pretendard-Regular", + fontSize: sizeToken.fontSize, + color: "#040404", + }} + renderItem={(item: SelectItem) => { + return ( + + + {item.label} + + + ); + }} + /> + + + {errorText ? ( + {errorText} + ) : helperText ? ( + + {helperText} + + ) : null} + + ); +} + diff --git a/src/shared/ui/select/index.ts b/src/shared/ui/select/index.ts new file mode 100644 index 0000000..4ff96d8 --- /dev/null +++ b/src/shared/ui/select/index.ts @@ -0,0 +1,3 @@ +export { Select } from "./Select"; +export type { SelectItem, SelectProps, SelectSize } from "./types"; + diff --git a/src/shared/ui/select/types.ts b/src/shared/ui/select/types.ts new file mode 100644 index 0000000..131e413 --- /dev/null +++ b/src/shared/ui/select/types.ts @@ -0,0 +1,40 @@ +export type SelectItem = { + label: string; + value: string; + disabled?: boolean; +}; + +export type SelectSize = "sm" | "md"; + +export type SelectProps = { + /** 옵션 목록 */ + items: SelectItem[]; + + /** 선택된 값 (없으면 null) */ + value: string | null; + + /** 값 변경 콜백 */ + onChange: (value: string | null) => void; + + /** placeholder 텍스트 */ + placeholder?: string; + + /** 비활성화 */ + disabled?: boolean; + + /** (옵션) 상단 라벨 */ + label?: string; + + /** (옵션) 도움말 */ + helperText?: string; + + /** (옵션) 에러 문구. 값이 있으면 에러 상태로 렌더링 */ + errorText?: string; + + /** 사이즈 */ + size?: SelectSize; + + /** 테스트용 id */ + testID?: string; +}; + diff --git a/yarn.lock b/yarn.lock index 6e70ff7..b534dab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4966,6 +4966,11 @@ lodash.throttle@^4.1.1: resolved "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz" integrity sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ== +lodash@^4.17.21: + version "4.17.23" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.23.tgz#f113b0378386103be4f6893388c73d0bde7f2c5a" + integrity sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w== + log-symbols@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz" @@ -5960,6 +5965,13 @@ react-native-css-interop@0.2.1: lightningcss "~1.27.0" semver "^7.6.3" +react-native-element-dropdown@^2.12.4: + version "2.12.4" + resolved "https://registry.yarnpkg.com/react-native-element-dropdown/-/react-native-element-dropdown-2.12.4.tgz#259db84e673b9f4fc03090a54760efd4f4a1456c" + integrity sha512-abZc5SVji9FIt7fjojRYrbuvp03CoeZJrgvezQoDoSOrpiTqkX69ix5m+j06W2AVncA0VWvbT+vCMam8SoVadw== + dependencies: + lodash "^4.17.21" + react-native-gesture-handler@~2.28.0: version "2.28.0" resolved "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.28.0.tgz" From a6a3edf14433767c5f6f0964f47a25dc788e5e4f Mon Sep 17 00:00:00 2001 From: chunjaemin Date: Sun, 15 Feb 2026 23:12:53 +0900 Subject: [PATCH 2/3] =?UTF-8?q?[FEAT/#6]=20=20=EB=93=9C=EB=A1=AD=EB=8B=A4?= =?UTF-8?q?=EC=9A=B4=20UI=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20styleSheet?= =?UTF-8?q?=EC=9A=A9=20=EB=94=94=EC=9E=90=EC=9D=B8=20=ED=86=A0=ED=81=B0?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(tabs)/_layout.tsx | 7 ++ src/app/(tabs)/playground.tsx | 56 +++++++++++ src/shared/styles/tokens.ts | 34 +++++++ src/shared/ui/select/Select.tsx | 165 ++++++++++++++++++-------------- 4 files changed, 191 insertions(+), 71 deletions(-) create mode 100644 src/app/(tabs)/playground.tsx create mode 100644 src/shared/styles/tokens.ts diff --git a/src/app/(tabs)/_layout.tsx b/src/app/(tabs)/_layout.tsx index ef71d9f..90fc54e 100644 --- a/src/app/(tabs)/_layout.tsx +++ b/src/app/(tabs)/_layout.tsx @@ -10,6 +10,13 @@ export default function TabLayout() { tabBarIcon: () => null, }} /> + null, + }} + /> ); } diff --git a/src/app/(tabs)/playground.tsx b/src/app/(tabs)/playground.tsx new file mode 100644 index 0000000..fbbc7d1 --- /dev/null +++ b/src/app/(tabs)/playground.tsx @@ -0,0 +1,56 @@ +/** + * ⚠️ 임시 Playground 페이지 + * - 컴포넌트 UI 확인용으로만 사용 + * - PR/배포 전 삭제 권장 + * + * 접근 경로: /playground + */ + +import { useMemo, useState } from "react"; +import { ScrollView, Text, View } from "react-native"; +import { Link } from "expo-router"; +import { Select } from "@/shared/ui"; + +export default function PlaygroundScreen() { + const councilItems = useMemo( + () => [ + { label: "총학생회", value: "university" }, + { label: "단과대학 학생회", value: "college" }, + { label: "학과/부 학생회", value: "department" }, + ], + [] + ); + + const [councilType, setCouncilType] = useState(null); + + return ( + + + + Playground + + + 임시 UI 확인 화면입니다. 작업 후 삭제하세요. + + + ← 홈으로 + + + + + - - 선택된 값: {councilType ?? "없음"} - - - - ); -} -