@uniai-fe/uds-primitives 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/styles.css +105 -13
- package/package.json +5 -3
- package/src/components/button/index.tsx +0 -2
- package/src/components/button/markup/Base.tsx +22 -1
- package/src/components/button/styles/button.scss +24 -2
- package/src/components/button/styles/variables.scss +4 -0
- package/src/components/button/types/index.ts +7 -0
- package/src/components/checkbox/markup/Checkbox.tsx +31 -25
- package/src/components/dropdown/markup/Template.tsx +4 -8
- package/src/components/dropdown/markup/foundation/Container.tsx +35 -7
- package/src/components/dropdown/markup/foundation/MenuItem.tsx +10 -10
- package/src/components/dropdown/markup/index.tsx +10 -1
- package/src/components/dropdown/styles/dropdown.scss +2 -2
- package/src/components/dropdown/styles/variables.scss +4 -4
- package/src/components/dropdown/types/base.ts +13 -0
- package/src/components/dropdown/types/props.ts +23 -27
- package/src/components/input/hooks/index.ts +1 -0
- package/src/components/input/hooks/useAddress.ts +247 -0
- package/src/components/input/index.scss +5 -1
- package/src/components/input/markup/address/Button.tsx +65 -0
- package/src/components/input/markup/address/Template.tsx +135 -0
- package/src/components/input/markup/address/index.ts +9 -0
- package/src/components/input/markup/foundation/Input.tsx +20 -1
- package/src/components/input/markup/index.tsx +2 -0
- package/src/components/input/styles/address.scss +24 -0
- package/src/components/input/styles/foundation.scss +28 -2
- package/src/components/input/styles/variables.scss +4 -0
- package/src/components/input/types/address.ts +249 -0
- package/src/components/input/types/foundation.ts +6 -0
- package/src/components/input/types/index.ts +1 -0
- package/src/components/input/utils/address.ts +165 -0
- package/src/components/input/utils/index.tsx +1 -0
- package/src/components/radio/markup/Radio.tsx +10 -2
- package/src/components/radio/markup/RadioCard.tsx +6 -1
- package/src/components/radio/markup/RadioCardGroup.tsx +6 -1
- package/src/components/select/markup/Default.tsx +6 -4
- package/src/components/select/markup/foundation/Container.tsx +23 -0
- package/src/components/select/markup/multiple/Multiple.tsx +6 -4
- package/src/components/select/styles/select.scss +25 -2
- package/src/components/select/styles/variables.scss +4 -0
- package/src/components/select/types/index.ts +1 -0
- package/src/components/select/types/option.ts +43 -0
- package/src/components/select/types/props.ts +29 -9
- package/src/components/input/styles/index.scss +0 -4
|
@@ -2,9 +2,35 @@
|
|
|
2
2
|
display: flex;
|
|
3
3
|
flex-direction: column;
|
|
4
4
|
gap: var(--spacing-gap-3);
|
|
5
|
-
width:
|
|
5
|
+
width: var(--input-width);
|
|
6
|
+
flex: var(--input-flex);
|
|
7
|
+
min-width: 0;
|
|
8
|
+
|
|
9
|
+
&[data-width="auto"] {
|
|
10
|
+
--input-width: auto;
|
|
11
|
+
--input-flex: 0 1 auto;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
&[data-width="fill"] {
|
|
15
|
+
--input-width: auto;
|
|
16
|
+
--input-flex: 1 1 0%;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
&[data-width="full"],
|
|
20
|
+
&[data-block="true"] {
|
|
21
|
+
--input-width: 100%;
|
|
22
|
+
--input-flex: 0 0 100%;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
&[data-width="fit"] {
|
|
26
|
+
--input-width: fit-content;
|
|
27
|
+
--input-flex: 0 0 auto;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
&[data-width="custom"] {
|
|
31
|
+
--input-flex: 0 0 auto;
|
|
32
|
+
}
|
|
6
33
|
|
|
7
|
-
&[data-block="true"],
|
|
8
34
|
&--block {
|
|
9
35
|
width: 100%;
|
|
10
36
|
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { Address } from "react-daum-postcode";
|
|
3
|
+
import type {
|
|
4
|
+
ButtonFill,
|
|
5
|
+
ButtonPriority,
|
|
6
|
+
ButtonSize,
|
|
7
|
+
} from "../../button/types";
|
|
8
|
+
import type { InputProps } from "./foundation";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 주소 문자열/배열 정규화 결과.
|
|
12
|
+
* @property {string} text 최종 병합된 주소 문자열
|
|
13
|
+
* @property {string[]} parts 주소를 구성하는 파트 배열
|
|
14
|
+
*/
|
|
15
|
+
export interface AddressResolvedValue {
|
|
16
|
+
/**
|
|
17
|
+
* 최종 병합된 주소 문자열
|
|
18
|
+
*/
|
|
19
|
+
text: string;
|
|
20
|
+
/**
|
|
21
|
+
* 주소 파트 배열
|
|
22
|
+
*/
|
|
23
|
+
parts: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 주소 검색 완료 데이터.
|
|
28
|
+
* @property {string} address 전체 주소 문자열
|
|
29
|
+
* @property {string[]} [addressParts] 주소를 구성하는 문자열 배열
|
|
30
|
+
* @property {AddressStructure} [addressStructure] 주소 구성 정보(기본/추가 파트)
|
|
31
|
+
* @property {string} zipCode 우편번호(5자리)
|
|
32
|
+
* @property {string} [buildingName] 건물명
|
|
33
|
+
* @property {string} [district] 법정동/읍면 정보
|
|
34
|
+
* @property {Address} raw react-daum-postcode Address 원본 데이터
|
|
35
|
+
*/
|
|
36
|
+
export interface AddressSearchResult {
|
|
37
|
+
/**
|
|
38
|
+
* 전체 주소 문자열
|
|
39
|
+
*/
|
|
40
|
+
address: string;
|
|
41
|
+
/**
|
|
42
|
+
* 주소를 구성하는 문자열 배열
|
|
43
|
+
*/
|
|
44
|
+
addressParts?: string[];
|
|
45
|
+
/**
|
|
46
|
+
* 주소 구성 정보(기본 주소 + 추가 파트)
|
|
47
|
+
*/
|
|
48
|
+
addressStructure?: AddressStructure;
|
|
49
|
+
/**
|
|
50
|
+
* 우편번호(5자리)
|
|
51
|
+
*/
|
|
52
|
+
zipCode: string;
|
|
53
|
+
/**
|
|
54
|
+
* 건물명
|
|
55
|
+
*/
|
|
56
|
+
buildingName?: string;
|
|
57
|
+
/**
|
|
58
|
+
* 법정동/읍면 정보
|
|
59
|
+
*/
|
|
60
|
+
district?: string;
|
|
61
|
+
/**
|
|
62
|
+
* react-daum-postcode Address 원본 데이터
|
|
63
|
+
*/
|
|
64
|
+
raw: Address;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 주소 구성 정보.
|
|
69
|
+
* @property {string} main 기본 주소
|
|
70
|
+
* @property {string[]} extras 추가 주소 파트 배열
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* const structure: AddressStructure = {
|
|
74
|
+
* main: "서울시 강남구 ...",
|
|
75
|
+
* extras: ["도곡동", "OO아파트"],
|
|
76
|
+
* };
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export interface AddressStructure {
|
|
80
|
+
/**
|
|
81
|
+
* 기본 주소
|
|
82
|
+
*/
|
|
83
|
+
main: string;
|
|
84
|
+
/**
|
|
85
|
+
* 추가 주소 파트 배열
|
|
86
|
+
*/
|
|
87
|
+
extras: string[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 주소 선택 처리 공통 옵션.
|
|
92
|
+
* @property {string} [addressFieldName] react-hook-form에 적용할 주소 필드 이름
|
|
93
|
+
* @property {string} [detailFieldName] react-hook-form에 적용할 상세 주소 필드 이름
|
|
94
|
+
* @property {string} [zipCodeFieldName] react-hook-form에 적용할 우편번호 필드 이름
|
|
95
|
+
* @property {boolean} [triggerValidation=true] setValue 이후 trigger 실행 여부
|
|
96
|
+
* @property {boolean} [resetDetailOnSelect=true] 새 주소 선택 시 상세 주소를 초기화할지 여부
|
|
97
|
+
* @property {(payload:AddressSearchResult)=>void} [onSelect] 주소 선택 후 콜백
|
|
98
|
+
*/
|
|
99
|
+
export interface AddressSelectionOptions {
|
|
100
|
+
/**
|
|
101
|
+
* react-hook-form 주소 필드 이름
|
|
102
|
+
*/
|
|
103
|
+
addressFieldName?: string;
|
|
104
|
+
/**
|
|
105
|
+
* react-hook-form 상세 주소 필드 이름
|
|
106
|
+
*/
|
|
107
|
+
detailFieldName?: string;
|
|
108
|
+
/**
|
|
109
|
+
* react-hook-form 우편번호 필드 이름
|
|
110
|
+
*/
|
|
111
|
+
zipCodeFieldName?: string;
|
|
112
|
+
/**
|
|
113
|
+
* setValue 이후 trigger 실행 여부
|
|
114
|
+
*/
|
|
115
|
+
triggerValidation?: boolean;
|
|
116
|
+
/**
|
|
117
|
+
* 새 주소 선택 시 상세 주소 초기화 여부
|
|
118
|
+
*/
|
|
119
|
+
resetDetailOnSelect?: boolean;
|
|
120
|
+
/**
|
|
121
|
+
* 주소 선택 후 콜백
|
|
122
|
+
*/
|
|
123
|
+
onSelect?: (payload: AddressSearchResult) => void;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 제한된 입력 스타일 props; priority/size/state/block만 공유한다.
|
|
128
|
+
*/
|
|
129
|
+
export type AddressSharedInputStyleProps = Pick<
|
|
130
|
+
InputProps,
|
|
131
|
+
"priority" | "size" | "state"
|
|
132
|
+
>;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 주소 검색 버튼 props.
|
|
136
|
+
* @property {ReactNode} [children] 버튼 라벨
|
|
137
|
+
* @property {ButtonPriority} [priority] 버튼 priority
|
|
138
|
+
* @property {ButtonFill} [fill] 버튼 fill 타입
|
|
139
|
+
* @property {ButtonSize} [size] 버튼 size
|
|
140
|
+
* @property {string} [className] 추가 className
|
|
141
|
+
* @property {"button"|"submit"|"reset"} [type] button type
|
|
142
|
+
* @property {boolean} [disabled] disabled 여부
|
|
143
|
+
* @property {string} [addressFieldName] react-hook-form에 적용할 주소 필드 이름
|
|
144
|
+
* @property {string} [zipCodeFieldName] react-hook-form에 적용할 우편번호 필드 이름
|
|
145
|
+
* @property {boolean} [triggerValidation=true] setValue 이후 trigger 실행 여부
|
|
146
|
+
* @property {(payload:AddressSearchResult)=>void} [onSelect] 주소 선택 후 콜백
|
|
147
|
+
*/
|
|
148
|
+
export interface AddressFindButtonProps extends AddressSelectionOptions {
|
|
149
|
+
/**
|
|
150
|
+
* 버튼 라벨
|
|
151
|
+
*/
|
|
152
|
+
label?: ReactNode;
|
|
153
|
+
/**
|
|
154
|
+
* 버튼 priority
|
|
155
|
+
*/
|
|
156
|
+
priority?: ButtonPriority;
|
|
157
|
+
/**
|
|
158
|
+
* 버튼 fill 타입
|
|
159
|
+
*/
|
|
160
|
+
fill?: ButtonFill;
|
|
161
|
+
/**
|
|
162
|
+
* 버튼 size
|
|
163
|
+
*/
|
|
164
|
+
size?: ButtonSize;
|
|
165
|
+
/**
|
|
166
|
+
* 추가 className
|
|
167
|
+
*/
|
|
168
|
+
className?: string;
|
|
169
|
+
/**
|
|
170
|
+
* button type
|
|
171
|
+
*/
|
|
172
|
+
type?: "button" | "submit" | "reset";
|
|
173
|
+
/**
|
|
174
|
+
* disabled 여부
|
|
175
|
+
*/
|
|
176
|
+
disabled?: boolean;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 주소 템플릿 버튼 옵션.
|
|
181
|
+
* @property {ReactNode} [label] 버튼 라벨
|
|
182
|
+
* @property {ButtonPriority} [priority] 버튼 priority
|
|
183
|
+
* @property {ButtonFill} [fill] 버튼 fill 타입
|
|
184
|
+
* @property {ButtonSize} [size] 버튼 size
|
|
185
|
+
* @property {boolean} [disabled] 버튼 disabled 여부
|
|
186
|
+
*/
|
|
187
|
+
export interface AddressTemplateButtonOptions {
|
|
188
|
+
/**
|
|
189
|
+
* 버튼 라벨
|
|
190
|
+
*/
|
|
191
|
+
label?: ReactNode;
|
|
192
|
+
/**
|
|
193
|
+
* 버튼 priority
|
|
194
|
+
*/
|
|
195
|
+
priority?: ButtonPriority;
|
|
196
|
+
/**
|
|
197
|
+
* 버튼 fill 타입
|
|
198
|
+
*/
|
|
199
|
+
fill?: ButtonFill;
|
|
200
|
+
/**
|
|
201
|
+
* 버튼 size
|
|
202
|
+
*/
|
|
203
|
+
size?: ButtonSize;
|
|
204
|
+
/**
|
|
205
|
+
* 버튼 disabled 여부
|
|
206
|
+
*/
|
|
207
|
+
disabled?: boolean;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 주소 필드 템플릿 props.
|
|
212
|
+
* @property {string} [className] container className
|
|
213
|
+
* @property {AddressSharedInputStyleProps} [inputStyle] 공통 입력 스타일 props
|
|
214
|
+
* @property {InputProps} addressInput 주소 입력 Input props
|
|
215
|
+
* @property {InputProps} [detailInput] 상세 주소 입력 props
|
|
216
|
+
* @property {string} [detailFieldName] react-hook-form에 적용할 상세 주소 필드 이름
|
|
217
|
+
* @property {AddressTemplateButtonOptions} [buttonProps] 버튼 옵션
|
|
218
|
+
* @property {boolean} [disabled] 템플릿 disabled 여부
|
|
219
|
+
* @property {string} [addressFieldName] react-hook-form에 적용할 주소 필드 이름
|
|
220
|
+
* @property {string} [zipCodeFieldName] react-hook-form에 적용할 우편번호 필드 이름
|
|
221
|
+
* @property {boolean} [triggerValidation=true] setValue 이후 trigger 실행 여부
|
|
222
|
+
* @property {(payload:AddressSearchResult)=>void} [onSelect] 주소 선택 후 콜백
|
|
223
|
+
*/
|
|
224
|
+
export interface AddressTemplateProps extends AddressSelectionOptions {
|
|
225
|
+
/**
|
|
226
|
+
* container className
|
|
227
|
+
*/
|
|
228
|
+
className?: string;
|
|
229
|
+
/**
|
|
230
|
+
* 공통 입력 스타일 props
|
|
231
|
+
*/
|
|
232
|
+
inputStyle?: AddressSharedInputStyleProps;
|
|
233
|
+
/**
|
|
234
|
+
* 주소 입력 Input props
|
|
235
|
+
*/
|
|
236
|
+
addressInput: InputProps;
|
|
237
|
+
/**
|
|
238
|
+
* 상세 주소 입력 Input props
|
|
239
|
+
*/
|
|
240
|
+
detailInput?: InputProps;
|
|
241
|
+
/**
|
|
242
|
+
* 버튼 옵션
|
|
243
|
+
*/
|
|
244
|
+
buttonProps?: AddressTemplateButtonOptions;
|
|
245
|
+
/**
|
|
246
|
+
* 템플릿 disabled 여부
|
|
247
|
+
*/
|
|
248
|
+
disabled?: boolean;
|
|
249
|
+
}
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
ReactNode,
|
|
6
6
|
} from "react";
|
|
7
7
|
import type { UseFormRegisterReturn } from "react-hook-form";
|
|
8
|
+
import type { FormFieldWidth } from "../../form/types/props";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* input; priority option
|
|
@@ -99,6 +100,7 @@ export interface InputIcon {
|
|
|
99
100
|
* @property {React.ReactNode} [clear] input reset버튼 커스텀 컨텐츠
|
|
100
101
|
* @property {React.ReactNode} [success] input 입력상태 성공시 커스텀 컨텐츠
|
|
101
102
|
* @property {React.ReactNode} [error] input 입력상태 에러시 커스텀 컨텐츠
|
|
103
|
+
* @property {FormFieldWidth} [width] width preset 옵션
|
|
102
104
|
*/
|
|
103
105
|
export interface InputProps extends Omit<NativeInputProps, "size">, InputIcon {
|
|
104
106
|
/**
|
|
@@ -129,6 +131,10 @@ export interface InputProps extends Omit<NativeInputProps, "size">, InputIcon {
|
|
|
129
131
|
* react-hook-form register 반환값
|
|
130
132
|
*/
|
|
131
133
|
register?: UseFormRegisterReturn;
|
|
134
|
+
/**
|
|
135
|
+
* width preset 옵션
|
|
136
|
+
*/
|
|
137
|
+
width?: FormFieldWidth;
|
|
132
138
|
/**
|
|
133
139
|
* Storybook 등에서 강제 상태 표현용
|
|
134
140
|
*/
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { string as toString } from "@uniai-fe/util-functions";
|
|
2
|
+
import type { Address } from "react-daum-postcode";
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
AddressResolvedValue,
|
|
6
|
+
AddressSearchResult,
|
|
7
|
+
AddressStructure,
|
|
8
|
+
} from "../types/address";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 객체 여부 체크 util.
|
|
12
|
+
*/
|
|
13
|
+
const isObject = (value: unknown): value is Record<string, unknown> =>
|
|
14
|
+
typeof value === "object" && value !== null;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* react-daum-postcode AddressSearchResult 유사 객체인지 검사한다.
|
|
18
|
+
*/
|
|
19
|
+
const isAddressSearchResultLike = (
|
|
20
|
+
value: unknown,
|
|
21
|
+
): value is Partial<AddressSearchResult> =>
|
|
22
|
+
isObject(value) && typeof value.address === "string";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* AddressStructure 형태인지 검사한다.
|
|
26
|
+
*/
|
|
27
|
+
const isAddressStructureLike = (
|
|
28
|
+
value: unknown,
|
|
29
|
+
): value is Partial<AddressStructure> =>
|
|
30
|
+
isObject(value) &&
|
|
31
|
+
("main" in value || "extras" in value) &&
|
|
32
|
+
(typeof value.main === "string" || Array.isArray(value.extras));
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 문자열 파트 배열을 안전하게 변환한다.
|
|
36
|
+
*/
|
|
37
|
+
const toAddressParts = (items: unknown[] | undefined): string[] => {
|
|
38
|
+
if (!Array.isArray(items)) return [];
|
|
39
|
+
return items
|
|
40
|
+
.map(item => toString(item, ""))
|
|
41
|
+
.filter((segment): segment is string => segment.length > 0);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* AddressStructure에서 main/extras를 순서대로 병합한다.
|
|
46
|
+
*/
|
|
47
|
+
const mergeStructureParts = (
|
|
48
|
+
structure: AddressStructure | Partial<AddressStructure>,
|
|
49
|
+
): string[] => {
|
|
50
|
+
const mainPart =
|
|
51
|
+
typeof structure.main === "string" ? structure.main : undefined;
|
|
52
|
+
const extras = toAddressParts(structure.extras);
|
|
53
|
+
return [mainPart, ...extras].filter(
|
|
54
|
+
(part): part is string => typeof part === "string" && part.length > 0,
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Template/Base 컴포넌트에서 사용하는 주소 문자열 정규화.
|
|
60
|
+
* @function
|
|
61
|
+
* @param {unknown} value RHF watch 값 혹은 addressInput.value override
|
|
62
|
+
* @returns {AddressResolvedValue} 문자열과 파트 배열
|
|
63
|
+
*/
|
|
64
|
+
export const resolveAddressValue = (value: unknown): AddressResolvedValue => {
|
|
65
|
+
if (typeof value === "string") {
|
|
66
|
+
return {
|
|
67
|
+
text: value,
|
|
68
|
+
parts: value.length > 0 ? [value] : [],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (Array.isArray(value)) {
|
|
73
|
+
const parts = toAddressParts(value);
|
|
74
|
+
return {
|
|
75
|
+
text: parts.join(" "),
|
|
76
|
+
parts,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (isAddressSearchResultLike(value)) {
|
|
81
|
+
const base = value.address ?? "";
|
|
82
|
+
const parts =
|
|
83
|
+
value.addressParts && value.addressParts.length > 0
|
|
84
|
+
? toAddressParts(value.addressParts)
|
|
85
|
+
: value.addressStructure
|
|
86
|
+
? mergeStructureParts(value.addressStructure)
|
|
87
|
+
: base
|
|
88
|
+
? [base]
|
|
89
|
+
: [];
|
|
90
|
+
return {
|
|
91
|
+
text: base,
|
|
92
|
+
parts,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (isAddressStructureLike(value)) {
|
|
97
|
+
const parts = mergeStructureParts(value);
|
|
98
|
+
return {
|
|
99
|
+
text: parts.join(" "),
|
|
100
|
+
parts,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (isObject(value) && "text" in value) {
|
|
105
|
+
const textValue =
|
|
106
|
+
typeof (value as { text?: unknown }).text === "string"
|
|
107
|
+
? (value as { text: string }).text
|
|
108
|
+
: "";
|
|
109
|
+
const parts = Array.isArray((value as { parts?: unknown[] }).parts)
|
|
110
|
+
? toAddressParts((value as { parts: unknown[] }).parts)
|
|
111
|
+
: textValue
|
|
112
|
+
? [textValue]
|
|
113
|
+
: [];
|
|
114
|
+
return {
|
|
115
|
+
text: textValue,
|
|
116
|
+
parts,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { text: "", parts: [] };
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* react-daum-postcode Address 데이터를 AddressSearchResult 형식으로 변환한다.
|
|
125
|
+
* `address` 필드는 우편번호 서비스에서 내려준 최종 문자열(도로명 + 추가 항목)을 그대로 유지하고,
|
|
126
|
+
* 분해 정보(`addressParts`, `addressStructure`)를 추가로 제공한다.
|
|
127
|
+
* @function
|
|
128
|
+
* @see https://www.juso.go.kr/addrlink/devAddrLinkRequestGuide.do
|
|
129
|
+
* @param {Address} data 우편번호 서비스 선택 결과
|
|
130
|
+
* @returns {AddressSearchResult} 변환된 주소 정보 객체
|
|
131
|
+
* @example
|
|
132
|
+
* ```ts
|
|
133
|
+
* const result = createAddressSearchResult(data);
|
|
134
|
+
* console.log(result.address); // "서울시 강남구 ... (OO동)"
|
|
135
|
+
* console.log(result.addressParts); // ["서울시 강남구 ...", "OO동"]
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
export const createAddressSearchResult = (
|
|
139
|
+
data: Address,
|
|
140
|
+
): AddressSearchResult => {
|
|
141
|
+
const extraParts: string[] = [];
|
|
142
|
+
|
|
143
|
+
if (data.addressType === "R") {
|
|
144
|
+
if (data.bname) extraParts.push(data.bname);
|
|
145
|
+
if (data.buildingName) extraParts.push(data.buildingName);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const baseAddress = data.address;
|
|
149
|
+
const labelExtras =
|
|
150
|
+
extraParts.length > 0 ? ` (${extraParts.join(", ")})` : "";
|
|
151
|
+
const parts = [baseAddress, ...extraParts].filter(part => part.length > 0);
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
address: `${baseAddress}${labelExtras}`,
|
|
155
|
+
addressParts: parts,
|
|
156
|
+
addressStructure: {
|
|
157
|
+
main: baseAddress,
|
|
158
|
+
extras: [...extraParts],
|
|
159
|
+
},
|
|
160
|
+
zipCode: data.zonecode,
|
|
161
|
+
buildingName: data.buildingName || undefined,
|
|
162
|
+
district: data.bname || undefined,
|
|
163
|
+
raw: data,
|
|
164
|
+
};
|
|
165
|
+
};
|
|
@@ -26,7 +26,7 @@ const RADIO_HELPER_CLASSNAME = "radio-helper";
|
|
|
26
26
|
* @example
|
|
27
27
|
* <Radio value="option" />
|
|
28
28
|
*/
|
|
29
|
-
|
|
29
|
+
const Radio = forwardRef<HTMLButtonElement, RadioProps>(function Radio(
|
|
30
30
|
{ size = "medium", className, disabled, ...restProps },
|
|
31
31
|
ref,
|
|
32
32
|
) {
|
|
@@ -49,6 +49,9 @@ export const Radio = forwardRef<HTMLButtonElement, RadioProps>(function Radio(
|
|
|
49
49
|
);
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
+
// forwardRef Tooltip 유지를 위해 displayName 지정.
|
|
53
|
+
Radio.displayName = "Radio";
|
|
54
|
+
|
|
52
55
|
/**
|
|
53
56
|
* RadioField component; label/helper wrapper for single radioItem
|
|
54
57
|
* @component
|
|
@@ -66,7 +69,7 @@ export const Radio = forwardRef<HTMLButtonElement, RadioProps>(function Radio(
|
|
|
66
69
|
* @example
|
|
67
70
|
* <RadioField label="옵션" helperText="필수" value="option" />
|
|
68
71
|
*/
|
|
69
|
-
|
|
72
|
+
const RadioField = forwardRef<HTMLButtonElement, RadioFieldProps>(
|
|
70
73
|
function RadioField(
|
|
71
74
|
{
|
|
72
75
|
label,
|
|
@@ -133,3 +136,8 @@ export const RadioField = forwardRef<HTMLButtonElement, RadioFieldProps>(
|
|
|
133
136
|
);
|
|
134
137
|
},
|
|
135
138
|
);
|
|
139
|
+
|
|
140
|
+
// forwardRef Tooltip 유지를 위해 displayName 지정.
|
|
141
|
+
RadioField.displayName = "RadioField";
|
|
142
|
+
|
|
143
|
+
export { Radio, RadioField };
|
|
@@ -30,7 +30,7 @@ const RADIO_CARD_INDICATOR_CLASSNAME = "radio-card-indicator";
|
|
|
30
30
|
* @example
|
|
31
31
|
* <RadioCard title="행복1농장" value="farm-1" />
|
|
32
32
|
*/
|
|
33
|
-
|
|
33
|
+
const RadioCard = forwardRef<HTMLButtonElement, RadioCardProps>(
|
|
34
34
|
function RadioCard(
|
|
35
35
|
{
|
|
36
36
|
title,
|
|
@@ -74,3 +74,8 @@ export const RadioCard = forwardRef<HTMLButtonElement, RadioCardProps>(
|
|
|
74
74
|
);
|
|
75
75
|
},
|
|
76
76
|
);
|
|
77
|
+
|
|
78
|
+
// forwardRef Tooltip 유지를 위해 displayName 지정.
|
|
79
|
+
RadioCard.displayName = "RadioCard";
|
|
80
|
+
|
|
81
|
+
export { RadioCard };
|
|
@@ -22,7 +22,7 @@ const RADIO_CARD_GROUP_CLASSNAME = "radio-card-group";
|
|
|
22
22
|
* @example
|
|
23
23
|
* <RadioCardGroup options={[{ id: "farm-1", title: "농장" }]} value="farm-1" />
|
|
24
24
|
*/
|
|
25
|
-
|
|
25
|
+
const RadioCardGroup = forwardRef<
|
|
26
26
|
ElementRef<typeof RadixRadioGroup.Root>,
|
|
27
27
|
RadioCardGroupProps
|
|
28
28
|
>(function RadioCardGroup(
|
|
@@ -81,3 +81,8 @@ export const RadioCardGroup = forwardRef<
|
|
|
81
81
|
</RadixRadioGroup.Root>
|
|
82
82
|
);
|
|
83
83
|
});
|
|
84
|
+
|
|
85
|
+
// forwardRef Tooltip 유지를 위해 displayName 지정.
|
|
86
|
+
RadioCardGroup.displayName = "RadioCardGroup";
|
|
87
|
+
|
|
88
|
+
export { RadioCardGroup };
|
|
@@ -36,6 +36,7 @@ const SelectDefault = forwardRef<HTMLElement, SelectDefaultComponentProps>(
|
|
|
36
36
|
size = "medium",
|
|
37
37
|
state = "default",
|
|
38
38
|
block,
|
|
39
|
+
width,
|
|
39
40
|
isOpen,
|
|
40
41
|
disabled,
|
|
41
42
|
buttonType,
|
|
@@ -43,7 +44,7 @@ const SelectDefault = forwardRef<HTMLElement, SelectDefaultComponentProps>(
|
|
|
43
44
|
selectedOptionIds,
|
|
44
45
|
onOptionSelect,
|
|
45
46
|
dropdownSize,
|
|
46
|
-
|
|
47
|
+
dropdownWidth = "match",
|
|
47
48
|
dropdownRootProps,
|
|
48
49
|
dropdownContainerProps,
|
|
49
50
|
dropdownMenuListProps,
|
|
@@ -86,6 +87,7 @@ const SelectDefault = forwardRef<HTMLElement, SelectDefaultComponentProps>(
|
|
|
86
87
|
<Container
|
|
87
88
|
className={clsx("select-trigger-container", className)}
|
|
88
89
|
block={block}
|
|
90
|
+
width={width}
|
|
89
91
|
>
|
|
90
92
|
<Dropdown.Root
|
|
91
93
|
open={dropdownOpen}
|
|
@@ -117,7 +119,7 @@ const SelectDefault = forwardRef<HTMLElement, SelectDefaultComponentProps>(
|
|
|
117
119
|
<Dropdown.Container
|
|
118
120
|
{...dropdownContainerProps}
|
|
119
121
|
size={panelSize}
|
|
120
|
-
|
|
122
|
+
width={dropdownWidth}
|
|
121
123
|
>
|
|
122
124
|
<Dropdown.Menu.List {...dropdownMenuListProps}>
|
|
123
125
|
{/* Dropdown menu option들을 그대로 매핑해 선택 이벤트를 전달한다. */}
|
|
@@ -127,8 +129,8 @@ const SelectDefault = forwardRef<HTMLElement, SelectDefaultComponentProps>(
|
|
|
127
129
|
label={option.label}
|
|
128
130
|
description={option.description}
|
|
129
131
|
disabled={option.disabled}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
+
left={option.left}
|
|
133
|
+
right={option.right}
|
|
132
134
|
multiple={Boolean(option.multiple)}
|
|
133
135
|
isSelected={resolvedSelectedIds.includes(option.id)}
|
|
134
136
|
onSelect={event => {
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
import clsx from "clsx";
|
|
4
4
|
|
|
5
5
|
import type { SelectContainerProps } from "../../types/props";
|
|
6
|
+
import {
|
|
7
|
+
getFormFieldWidthAttr,
|
|
8
|
+
getFormFieldWidthValue,
|
|
9
|
+
} from "../../../form/utils/form-field";
|
|
6
10
|
|
|
7
11
|
/**
|
|
8
12
|
* Select trigger/container wrapper
|
|
@@ -16,12 +20,31 @@ export default function SelectContainer({
|
|
|
16
20
|
className,
|
|
17
21
|
children,
|
|
18
22
|
block = false,
|
|
23
|
+
width,
|
|
24
|
+
style,
|
|
25
|
+
...restProps
|
|
19
26
|
}: SelectContainerProps) {
|
|
27
|
+
const widthAttr =
|
|
28
|
+
width !== undefined
|
|
29
|
+
? getFormFieldWidthAttr(width)
|
|
30
|
+
: block
|
|
31
|
+
? "full"
|
|
32
|
+
: undefined;
|
|
33
|
+
const widthValue =
|
|
34
|
+
width !== undefined ? getFormFieldWidthValue(width) : undefined;
|
|
35
|
+
const mergedStyle =
|
|
36
|
+
widthValue !== undefined
|
|
37
|
+
? { ...(style ?? {}), ["--select-width" as const]: widthValue }
|
|
38
|
+
: style;
|
|
39
|
+
|
|
20
40
|
return (
|
|
21
41
|
<div
|
|
22
42
|
className={clsx("select select-container", className, {
|
|
23
43
|
"select-block": block,
|
|
24
44
|
})}
|
|
45
|
+
data-width={widthAttr}
|
|
46
|
+
style={mergedStyle}
|
|
47
|
+
{...restProps}
|
|
25
48
|
>
|
|
26
49
|
{/** dropdown root 및 dropdown menu 등 포함 예정 */}
|
|
27
50
|
{children}
|