@uniai-fe/uds-templates 0.4.2 → 0.4.3

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/README.md CHANGED
@@ -55,6 +55,15 @@ Next.js 서비스에서 primitives와 동일한 방식으로 **Raw TypeScript**
55
55
  - `weatherCoordinate`
56
56
  - `useWeatherKorea`
57
57
  - `useOpenWeatherMap`
58
+ - `/service-inquiry`
59
+ - `ServiceInquiry.Form`
60
+ - `ServiceInquiry.OpenButton`
61
+ - `ServiceInquiry.useOpen`
62
+ - `ServiceInquiry.useUserContext`
63
+ - `ServiceInquiry.createModal`
64
+ - `ServiceInquiryFieldMode`
65
+ - `ServiceInquiryFormProps`
66
+ - `ServiceInquiryFormValues`
58
67
  - `/cctv`
59
68
  - `CCTV.Provider`
60
69
  - `CCTV.CamList.Container`
@@ -95,6 +104,10 @@ Next.js 서비스에서 primitives와 동일한 방식으로 **Raw TypeScript**
95
104
  - `/modal/**`
96
105
  - ui-legacy 스택 기반 모달 Provider/Root/Container + 템플릿(`Modal.Alert`, `Modal.Dialog`)
97
106
  - Storybook(`apps/design-storybook/src/stories/templates/modal`)에서 Alert/Confirm 케이스를 검증한다.
107
+ - `/service-inquiry/**`
108
+ - 문의 입력 전용 form, 기본 원형 `?` open button, 커스텀 trigger용 open hook, request context 조립 hook, modal preset factory를 제공한다.
109
+ - submit transport, React Query mutation, Next.js route handler, 에러 피드백(`Modal.Alert`)은 서비스 앱이 소유한다.
110
+ - 로그인 후 `farm_name`, `contact`를 auto-fill + readonly로 보여야 할 때는 `formContextOptions.defaultValues`와 `farmNameField.mode`, `contactField.mode`를 함께 전달한다.
98
111
  - `/weather/**`
99
112
  - page-frame header utility에 결합되는 weather header 템플릿과 weather data hook/mock 도구를 제공한다.
100
113
  - `/cctv/**`
@@ -104,6 +117,16 @@ Next.js 서비스에서 primitives와 동일한 방식으로 **Raw TypeScript**
104
117
 
105
118
  각 템플릿의 상세한 범위와 의사결정은 `CONTEXT-*.md` 문서에서 관리합니다.
106
119
 
120
+ ### Service Inquiry 도입 흐름
121
+
122
+ 1. 서비스 앱이 `react-hook-form`으로 `defaultValues`와 `onSubmit`을 준비한다.
123
+ 2. 필요하면 `ServiceInquiry.useUserContext`로 `labels`, `page_path`, 추가 `user_context`를 조립한다.
124
+ 3. 기본 버튼은 `ServiceInquiry.OpenButton`, 커스텀 버튼은 `ServiceInquiry.useOpen`으로 모달 open을 연결한다.
125
+ 4. modal footer confirm이 `ServiceInquiry.Form` submit 진입을 담당한다.
126
+ 5. 실제 submit은 서비스 앱의 `useMutation + Next.js route handler + Modal.Alert` 조합으로 처리한다.
127
+
128
+ `service-inquiry`는 구조와 request context까지만 제공하고, 네트워크 상태/재시도/성공·실패 피드백은 서비스 앱이 소유합니다.
129
+
107
130
  ### 회원가입 Step 구조
108
131
 
109
132
  1. **Step1 — User Info (name + phone)**: 기본 정보 입력. PhoneInput은 인증 UI 없이 마스킹만 제공한다.
package/dist/styles.css CHANGED
@@ -1732,3 +1732,35 @@
1732
1732
  .cctv-viewer-desktop-pagination-container {
1733
1733
  margin-top: var(--spacing-gap-8);
1734
1734
  }
1735
+
1736
+ .service-inquiry-form {
1737
+ display: flex;
1738
+ flex-direction: column;
1739
+ }
1740
+
1741
+ .service-inquiry-fields {
1742
+ display: flex;
1743
+ flex-direction: column;
1744
+ gap: var(--spacing-padding-5);
1745
+ }
1746
+
1747
+ .service-inquiry-field {
1748
+ display: flex;
1749
+ flex-direction: column;
1750
+ }
1751
+
1752
+ .service-inquiry-open-button {
1753
+ width: 40px;
1754
+ height: 40px;
1755
+ border-radius: 20px;
1756
+ border: 1px solid var(--color-border-standard-cool-gray);
1757
+ background: var(--color_100);
1758
+ color: var(--color-label-neutral);
1759
+ font-size: 24px;
1760
+ font-weight: 400;
1761
+ display: flex;
1762
+ align-items: center;
1763
+ justify-content: center;
1764
+ padding: 0;
1765
+ cursor: pointer;
1766
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniai-fe/uds-templates",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "UNIAI Design System; UI Templates Package",
5
5
  "type": "module",
6
6
  "private": false,
package/src/index.scss CHANGED
@@ -9,3 +9,4 @@
9
9
  @use "./auth/index.scss" as authStyles;
10
10
  @use "./weather/index.scss" as weatherStyles;
11
11
  @use "./cctv/index.scss" as cctvStyles;
12
+ @use "./service-inquiry/index.scss" as serviceInquiryStyles;
package/src/index.tsx CHANGED
@@ -6,5 +6,6 @@ export * from "./auth";
6
6
  export * from "./modal";
7
7
  export * from "./weather";
8
8
  export * from "./cctv";
9
+ export * from "./service-inquiry";
9
10
 
10
11
  export type * from "./types";
@@ -0,0 +1,157 @@
1
+ "use client";
2
+
3
+ import clsx from "clsx";
4
+ import { Form, Input } from "@uniai-fe/uds-primitives";
5
+ import { useFormContext } from "react-hook-form";
6
+ import type { ServiceInquiryFieldKey, ServiceInquiryFormProps } from "../types";
7
+ import type { ServiceInquiryFormValues } from "../types";
8
+
9
+ /**
10
+ * Service Inquiry Form; 문의 입력 form
11
+ * @component
12
+ * @param {ServiceInquiryFormProps} props 문의 form props
13
+ * @param {string} [props.className] form className
14
+ * @param {ServiceInquiryFieldKey[]} [props.visibleFields] 노출 필드 목록
15
+ * @param {ServiceInquiryInputFieldProps} [props.farmNameField] 농장명 필드 설정
16
+ * @param {ServiceInquiryInputFieldProps} [props.contactField] 연락처 필드 설정
17
+ * @param {ServiceInquiryTextAreaFieldProps} [props.textField] 문의 본문 필드 설정
18
+ * @param {SubmitHandler<ServiceInquiryFormValues>} props.onSubmit 문의 제출 핸들러
19
+ * @example
20
+ * <ServiceInquiryForm onSubmit={values => console.info(values)} />
21
+ */
22
+ const ServiceInquiryForm = ({
23
+ className,
24
+ visibleFields,
25
+ farmNameField,
26
+ contactField,
27
+ textField,
28
+ onSubmit,
29
+ }: ServiceInquiryFormProps) => {
30
+ const form = useFormContext<ServiceInquiryFormValues>();
31
+
32
+ // 변경 설명: Modal.Dialog confirm 기본 submit과 연결되도록 form.handleSubmit 결과를 그대로 onSubmit에 바인딩한다.
33
+ const handleSubmit = form.handleSubmit(onSubmit);
34
+
35
+ return (
36
+ <form
37
+ className={clsx("service-inquiry-form", className)}
38
+ onSubmit={handleSubmit}
39
+ >
40
+ <div className="service-inquiry-fields">
41
+ {(visibleFields?.includes("farm_name") ?? true) ? (
42
+ <Form.Field.Template
43
+ className={clsx(
44
+ "service-inquiry-field",
45
+ "service-inquiry-field-farm-name",
46
+ farmNameField?.className,
47
+ )}
48
+ width={farmNameField?.inputProps?.width ?? "full"}
49
+ headerProps={{
50
+ required: farmNameField?.required,
51
+ ...(typeof farmNameField?.label === "string"
52
+ ? { label: farmNameField.label }
53
+ : typeof farmNameField?.label === "number"
54
+ ? { label: String(farmNameField.label) }
55
+ : {
56
+ labelJsx:
57
+ typeof farmNameField?.label === "undefined"
58
+ ? "농장명"
59
+ : farmNameField.label,
60
+ }),
61
+ }}
62
+ footer={farmNameField?.helper}
63
+ >
64
+ <Input.Base
65
+ type="text"
66
+ block={farmNameField?.inputProps?.block ?? true}
67
+ readOnly={
68
+ farmNameField?.mode === "readonly" ||
69
+ farmNameField?.inputProps?.readOnly === true
70
+ }
71
+ placeholder={farmNameField?.placeholder ?? "농장명 입력"}
72
+ {...farmNameField?.inputProps}
73
+ register={form.register("farm_name")}
74
+ />
75
+ </Form.Field.Template>
76
+ ) : null}
77
+
78
+ {(visibleFields?.includes("contact") ?? true) ? (
79
+ <Form.Field.Template
80
+ className={clsx(
81
+ "service-inquiry-field",
82
+ "service-inquiry-field-contact",
83
+ contactField?.className,
84
+ )}
85
+ width={contactField?.inputProps?.width ?? "full"}
86
+ headerProps={{
87
+ required: contactField?.required ?? true,
88
+ ...(typeof contactField?.label === "string"
89
+ ? { label: contactField.label }
90
+ : typeof contactField?.label === "number"
91
+ ? { label: String(contactField.label) }
92
+ : {
93
+ labelJsx:
94
+ typeof contactField?.label === "undefined"
95
+ ? "연락처"
96
+ : contactField.label,
97
+ }),
98
+ }}
99
+ footer={contactField?.helper}
100
+ >
101
+ <Input.Base
102
+ type="text"
103
+ block={contactField?.inputProps?.block ?? true}
104
+ readOnly={
105
+ contactField?.mode === "readonly" ||
106
+ contactField?.inputProps?.readOnly === true
107
+ }
108
+ placeholder={
109
+ contactField?.placeholder ?? "이메일 또는 전화번호 입력"
110
+ }
111
+ {...contactField?.inputProps}
112
+ register={form.register("contact")}
113
+ />
114
+ </Form.Field.Template>
115
+ ) : null}
116
+
117
+ {(visibleFields?.includes("text") ?? true) ? (
118
+ <Form.Field.Template
119
+ className={clsx(
120
+ "service-inquiry-field",
121
+ "service-inquiry-field-text",
122
+ textField?.className,
123
+ )}
124
+ width={textField?.inputProps?.width ?? "full"}
125
+ headerProps={{
126
+ required: textField?.required ?? true,
127
+ ...(typeof textField?.label === "string"
128
+ ? { label: textField.label }
129
+ : typeof textField?.label === "number"
130
+ ? { label: String(textField.label) }
131
+ : {
132
+ labelJsx:
133
+ typeof textField?.label === "undefined"
134
+ ? "문의 내용"
135
+ : textField.label,
136
+ }),
137
+ }}
138
+ footer={textField?.helper}
139
+ >
140
+ <Input.TextArea
141
+ block={textField?.inputProps?.block ?? true}
142
+ placeholder={
143
+ textField?.placeholder ?? "문의 내용을 자세히 입력해 주세요."
144
+ }
145
+ height={textField?.inputProps?.height ?? 160}
146
+ length={textField?.inputProps?.length ?? 10000}
147
+ {...textField?.inputProps}
148
+ register={form.register("text")}
149
+ />
150
+ </Form.Field.Template>
151
+ ) : null}
152
+ </div>
153
+ </form>
154
+ );
155
+ };
156
+
157
+ export default ServiceInquiryForm;
@@ -0,0 +1,46 @@
1
+ "use client";
2
+
3
+ import { useOpenServiceInquiry } from "../hooks";
4
+ import type { ServiceInquiryOpenButtonProps } from "../types";
5
+
6
+ /**
7
+ * Service Inquiry Open Button; 문의 모달 trigger adapter
8
+ * @component
9
+ * @param {ServiceInquiryOpenButtonProps} props 문의 모달 열기 props
10
+ * @param {string} props.stackKey modal stack key
11
+ * @param {ServiceInquiryFormProps} props.formProps 문의 form props
12
+ * @param {Partial<DialogTemplateOptions<ServiceInquiryFormValues>>} [props.dialogOptions] 문의 모달 preset 위에 덮어쓸 dialog option
13
+ * @param {() => void} [props.onOpen] 모달 open 직전 콜백
14
+ * @example
15
+ * <ServiceInquiryOpenButton
16
+ * stackKey="sample"
17
+ * formProps={{ onSubmit: values => console.info(values) }}
18
+ * />
19
+ */
20
+ const ServiceInquiryOpenButton = ({
21
+ stackKey,
22
+ formProps,
23
+ dialogOptions,
24
+ onOpen,
25
+ }: ServiceInquiryOpenButtonProps) => {
26
+ const { openServiceInquiry } = useOpenServiceInquiry({
27
+ stackKey,
28
+ formProps,
29
+ dialogOptions,
30
+ onOpen,
31
+ });
32
+
33
+ return (
34
+ <button
35
+ type="button"
36
+ className="service-inquiry-open-button"
37
+ aria-label="문의하기"
38
+ // 변경 설명: 기본 제공 버튼은 고정 원형 `?` 버튼 사양으로 렌더링한다.
39
+ onClick={openServiceInquiry}
40
+ >
41
+ ?
42
+ </button>
43
+ );
44
+ };
45
+
46
+ export default ServiceInquiryOpenButton;
@@ -0,0 +1,2 @@
1
+ export * from "./useOpenServiceInquiry";
2
+ export * from "./useServiceInquiryUserContext";
@@ -0,0 +1,48 @@
1
+ "use client";
2
+
3
+ import { Modal } from "../../modal";
4
+ import { createServiceInquiryModal } from "../utils/modal-option";
5
+ import type {
6
+ UseOpenServiceInquiryOptions,
7
+ UseOpenServiceInquiryReturn,
8
+ } from "../types";
9
+
10
+ /**
11
+ * Service Inquiry Hook; 문의 모달 열기 Hook
12
+ * @hook
13
+ * @param {UseOpenServiceInquiryOptions} options 문의 모달 열기 훅 옵션
14
+ * @param {string} options.stackKey modal stack key
15
+ * @param {ServiceInquiryFormProps} options.formProps 문의 form props
16
+ * @param {Partial<DialogTemplateOptions<ServiceInquiryFormValues>>} [options.dialogOptions] 문의 모달 preset 위에 덮어쓸 dialog option
17
+ * @param {() => void} [options.onOpen] 모달 open 직전 콜백
18
+ * @returns {UseOpenServiceInquiryReturn} 문의 모달 open 함수
19
+ * @example
20
+ * const { openServiceInquiry } = useOpenServiceInquiry({
21
+ * stackKey: "sample",
22
+ * formProps: { onSubmit: values => console.info(values) },
23
+ * });
24
+ */
25
+ export function useOpenServiceInquiry({
26
+ stackKey,
27
+ formProps,
28
+ dialogOptions,
29
+ onOpen,
30
+ }: UseOpenServiceInquiryOptions): UseOpenServiceInquiryReturn {
31
+ const { newModal } = Modal.useModal();
32
+
33
+ // 변경 설명: 커스텀 버튼은 render prop 대신 hook이 돌려주는 단일 open 함수로 연결한다.
34
+ const openServiceInquiry = () => {
35
+ onOpen?.();
36
+ newModal(
37
+ createServiceInquiryModal({
38
+ stackKey,
39
+ formProps,
40
+ dialogOptions,
41
+ }),
42
+ );
43
+ };
44
+
45
+ return {
46
+ openServiceInquiry,
47
+ };
48
+ }
@@ -0,0 +1,89 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import type {
5
+ ServiceInquiryRequestContext,
6
+ ServiceInquiryUserContext,
7
+ UseServiceInquiryUserContextOptions,
8
+ UseServiceInquiryUserContextReturn,
9
+ } from "../types";
10
+
11
+ /**
12
+ * Service Inquiry Hook; request context 제어 Hook
13
+ * @hook
14
+ * @param {UseServiceInquiryUserContextOptions} options user_context 훅 옵션
15
+ * @param {string[]} [options.labels] 문의 request에 포함할 labels
16
+ * @param {string} [options.pagePath] 고정 page_path
17
+ * @param {ServiceInquiryPagePathResolver} [options.resolvePagePath] page_path 동적 resolver
18
+ * @param {ServiceInquiryUserContext | null} [options.userContext] 서비스 기본 user_context
19
+ * @param {ServiceInquiryUserContextResolver} [options.resolveUserContext] 서비스 기본 user_context 동적 resolver
20
+ * @returns {UseServiceInquiryUserContextReturn} request context 및 추가 user_context 제어 함수
21
+ * @example
22
+ * const inquiryContext = useServiceInquiryUserContext({
23
+ * labels: ["help-desk"],
24
+ * resolveUserContext: () => ({ section: "dashboard" }),
25
+ * });
26
+ */
27
+ export function useServiceInquiryUserContext({
28
+ labels,
29
+ pagePath,
30
+ resolvePagePath,
31
+ userContext,
32
+ resolveUserContext,
33
+ }: UseServiceInquiryUserContextOptions): UseServiceInquiryUserContextReturn {
34
+ /** 1) form init — 서비스 기본 context와 별도 trigger 추가 context를 나눠 관리한다. */
35
+ const [additionalUserContext, setAdditionalUserContext] =
36
+ useState<ServiceInquiryUserContext | null>(null);
37
+
38
+ /** 2) register merge — 서비스 기본 context와 trigger 추가 context를 한 payload로 합친다. */
39
+ const resolvedPagePath =
40
+ pagePath ??
41
+ resolvePagePath?.() ??
42
+ (typeof window === "undefined" ? "" : window.location.pathname);
43
+ const resolvedBaseUserContext = userContext ?? resolveUserContext?.() ?? null;
44
+ const mergedUserContext =
45
+ resolvedBaseUserContext || additionalUserContext
46
+ ? {
47
+ ...(resolvedBaseUserContext ?? {}),
48
+ ...(additionalUserContext ?? {}),
49
+ }
50
+ : null;
51
+
52
+ /** 3) helper/state — submit에 바로 연결할 request context snapshot을 노출한다. */
53
+ const requestContext: ServiceInquiryRequestContext = {
54
+ labels: labels ?? [],
55
+ page_path: resolvedPagePath,
56
+ user_context: mergedUserContext,
57
+ };
58
+
59
+ /** 4) submit/액션 — trigger별 추가 context를 append/reset 할 수 있게 연다. */
60
+ const setNextAdditionalUserContext: UseServiceInquiryUserContextReturn["setAdditionalUserContext"] =
61
+ nextUserContext => {
62
+ setAdditionalUserContext(nextUserContext);
63
+ };
64
+
65
+ const appendUserContext: UseServiceInquiryUserContextReturn["appendUserContext"] =
66
+ (nextUserContext?: ServiceInquiryUserContext | null) => {
67
+ if (!nextUserContext) {
68
+ return;
69
+ }
70
+
71
+ setAdditionalUserContext(currentUserContext => ({
72
+ ...(currentUserContext ?? {}),
73
+ ...nextUserContext,
74
+ }));
75
+ };
76
+
77
+ const clearAdditionalUserContext: UseServiceInquiryUserContextReturn["clearAdditionalUserContext"] =
78
+ () => {
79
+ setAdditionalUserContext(null);
80
+ };
81
+
82
+ return {
83
+ requestContext,
84
+ additionalUserContext,
85
+ setAdditionalUserContext: setNextAdditionalUserContext,
86
+ appendUserContext,
87
+ clearAdditionalUserContext,
88
+ };
89
+ }
@@ -0,0 +1 @@
1
+ @use "./styles/index.scss";
@@ -0,0 +1,26 @@
1
+ import "./index.scss";
2
+ import ServiceInquiryForm from "./components/Form";
3
+ import ServiceInquiryOpenButton from "./components/OpenButton";
4
+ import { useOpenServiceInquiry, useServiceInquiryUserContext } from "./hooks";
5
+ import { createServiceInquiryModal } from "./utils/modal-option";
6
+
7
+ /**
8
+ * Service Inquiry; 문의 form + modal option 엔트리
9
+ * - 변경 설명: Step 2에서 Form과 Modal.Dialog 기반 factory를 첫 runtime 조각으로 연다.
10
+ */
11
+ export const ServiceInquiry = {
12
+ Form: ServiceInquiryForm,
13
+ OpenButton: ServiceInquiryOpenButton,
14
+ useOpen: useOpenServiceInquiry,
15
+ useUserContext: useServiceInquiryUserContext,
16
+ createModal: createServiceInquiryModal,
17
+ };
18
+
19
+ export {
20
+ ServiceInquiryForm,
21
+ ServiceInquiryOpenButton,
22
+ useOpenServiceInquiry,
23
+ useServiceInquiryUserContext,
24
+ createServiceInquiryModal,
25
+ };
26
+ export type * from "./types";
@@ -0,0 +1,15 @@
1
+ .service-inquiry-form {
2
+ display: flex;
3
+ flex-direction: column;
4
+ }
5
+
6
+ .service-inquiry-fields {
7
+ display: flex;
8
+ flex-direction: column;
9
+ gap: var(--spacing-padding-5);
10
+ }
11
+
12
+ .service-inquiry-field {
13
+ display: flex;
14
+ flex-direction: column;
15
+ }
@@ -0,0 +1,2 @@
1
+ @use "./form.scss";
2
+ @use "./open-button.scss";
@@ -0,0 +1,15 @@
1
+ .service-inquiry-open-button {
2
+ width: 40px;
3
+ height: 40px;
4
+ border-radius: 20px;
5
+ border: 1px solid var(--color-border-standard-cool-gray);
6
+ background: var(--color_100);
7
+ color: var(--color-label-neutral);
8
+ font-size: 24px;
9
+ font-weight: 400;
10
+ display: flex;
11
+ align-items: center;
12
+ justify-content: center;
13
+ padding: 0;
14
+ cursor: pointer;
15
+ }
@@ -0,0 +1,68 @@
1
+ import type { ServiceInquiryUserContext } from "./form-context";
2
+
3
+ /**
4
+ * Service Inquiry API; 문의 등록 요청
5
+ * @property {string} text 문의 본문
6
+ * @property {string} farm_name 문의 대상 농장명 또는 빈 문자열
7
+ * @property {string[]} labels JIRA label 목록
8
+ * @property {string} contact 문의자 연락처(email 또는 phone number)
9
+ * @property {string} page_path 문의하기를 연 페이지 경로
10
+ * @property {ServiceInquiryUserContext | null} user_context 문의 시점 사용자/환경 맥락
11
+ */
12
+ export interface API_Req_ServiceInquiry {
13
+ /**
14
+ * 문의 본문
15
+ */
16
+ text: string;
17
+ /**
18
+ * 문의 대상 농장명 또는 빈 문자열
19
+ */
20
+ farm_name: string;
21
+ /**
22
+ * JIRA label 목록
23
+ */
24
+ labels: string[];
25
+ /**
26
+ * 문의자 연락처
27
+ */
28
+ contact: string;
29
+ /**
30
+ * 문의하기를 연 페이지 경로
31
+ */
32
+ page_path: string;
33
+ /**
34
+ * 문의 시점 사용자/환경 맥락
35
+ */
36
+ user_context: ServiceInquiryUserContext | null;
37
+ }
38
+
39
+ /**
40
+ * Service Inquiry API; 문의 등록 응답
41
+ * @property {boolean} success 문의 등록 성공 여부
42
+ * @property {string} issue_key JIRA issue key
43
+ * @property {string} issue_url JIRA issue URL
44
+ * @property {string} summary JIRA issue 제목
45
+ * @property {string} message 서버 응답 메시지
46
+ */
47
+ export interface API_Res_ServiceInquiry {
48
+ /**
49
+ * 문의 등록 성공 여부
50
+ */
51
+ success: boolean;
52
+ /**
53
+ * JIRA issue key
54
+ */
55
+ issue_key: string;
56
+ /**
57
+ * JIRA issue URL
58
+ */
59
+ issue_url: string;
60
+ /**
61
+ * JIRA issue 제목
62
+ */
63
+ summary: string;
64
+ /**
65
+ * 서버 응답 메시지
66
+ */
67
+ message: string;
68
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Service Inquiry Form; 사용자 입력값
3
+ * @property {string} text 문의 본문
4
+ * @property {string} farm_name 농장명 또는 빈 문자열
5
+ * @property {string} contact 문의자 연락처
6
+ */
7
+ export interface ServiceInquiryFormValues {
8
+ /**
9
+ * 문의 본문
10
+ */
11
+ text: string;
12
+ /**
13
+ * 농장명 또는 빈 문자열
14
+ */
15
+ farm_name: string;
16
+ /**
17
+ * 문의자 연락처
18
+ */
19
+ contact: string;
20
+ }
21
+
22
+ /**
23
+ * Service Inquiry Form; 시스템 주입 request context
24
+ * @property {string[]} labels JIRA label 목록
25
+ * @property {string} page_path 문의를 연 페이지 경로
26
+ * @property {ServiceInquiryUserContext | null} user_context 문의 시점 사용자/환경 맥락
27
+ */
28
+ export interface ServiceInquiryRequestContext {
29
+ /**
30
+ * JIRA label 목록
31
+ */
32
+ labels: string[];
33
+ /**
34
+ * 문의를 연 페이지 경로
35
+ */
36
+ page_path: string;
37
+ /**
38
+ * 문의 시점 사용자/환경 맥락
39
+ */
40
+ user_context: ServiceInquiryUserContext | null;
41
+ }
42
+
43
+ /**
44
+ * Service Inquiry Form; 사용자/환경 맥락 payload
45
+ * @property {unknown} [key] 디버깅을 위한 사용자 동작/환경 스냅샷
46
+ */
47
+ // 변경 설명: user_context는 서비스별 arbitrary object 확장 지점이므로 Record 기반 의도를 타입 이름에서 바로 읽히게 맞춘다.
48
+ export interface ServiceInquiryUserContext extends Record<string, unknown> {}
@@ -0,0 +1,83 @@
1
+ import type {
2
+ ServiceInquiryRequestContext,
3
+ ServiceInquiryUserContext,
4
+ } from "./form-context";
5
+
6
+ /**
7
+ * Service Inquiry Hook; 페이지 경로 resolver
8
+ * @returns {string} 문의하기를 연 페이지 경로
9
+ */
10
+ export type ServiceInquiryPagePathResolver = () => string;
11
+
12
+ /**
13
+ * Service Inquiry Hook; 서비스 user context resolver
14
+ * @returns {ServiceInquiryUserContext | null} 서비스가 제공하는 기본 user_context
15
+ */
16
+ export type ServiceInquiryUserContextResolver =
17
+ () => ServiceInquiryUserContext | null;
18
+
19
+ /**
20
+ * Service Inquiry Hook; user_context 훅 옵션
21
+ * @property {string[]} [labels] 문의 request에 포함할 labels
22
+ * @property {string} [pagePath] 고정 page_path
23
+ * @property {ServiceInquiryPagePathResolver} [resolvePagePath] page_path 동적 resolver
24
+ * @property {ServiceInquiryUserContext | null} [userContext] 서비스 기본 user_context
25
+ * @property {ServiceInquiryUserContextResolver} [resolveUserContext] 서비스 기본 user_context 동적 resolver
26
+ */
27
+ export interface UseServiceInquiryUserContextOptions {
28
+ /**
29
+ * 문의 request에 포함할 labels
30
+ */
31
+ labels?: string[];
32
+ /**
33
+ * 고정 page_path
34
+ */
35
+ pagePath?: string;
36
+ /**
37
+ * page_path 동적 resolver
38
+ */
39
+ resolvePagePath?: ServiceInquiryPagePathResolver;
40
+ /**
41
+ * 서비스 기본 user_context
42
+ */
43
+ userContext?: ServiceInquiryUserContext | null;
44
+ /**
45
+ * 서비스 기본 user_context 동적 resolver
46
+ */
47
+ resolveUserContext?: ServiceInquiryUserContextResolver;
48
+ }
49
+
50
+ /**
51
+ * Service Inquiry Hook; user_context 훅 반환값
52
+ * @property {ServiceInquiryRequestContext} requestContext form submit에 바로 사용할 request context
53
+ * @property {ServiceInquiryUserContext | null} additionalUserContext trigger/interaction에서 추가된 user_context
54
+ * @property {(nextUserContext: ServiceInquiryUserContext | null) => void} setAdditionalUserContext 추가 user_context 교체 함수
55
+ * @property {(nextUserContext?: ServiceInquiryUserContext | null) => void} appendUserContext 추가 user_context 병합 함수
56
+ * @property {() => void} clearAdditionalUserContext 추가 user_context 초기화 함수
57
+ */
58
+ export interface UseServiceInquiryUserContextReturn {
59
+ /**
60
+ * form submit에 바로 사용할 request context
61
+ */
62
+ requestContext: ServiceInquiryRequestContext;
63
+ /**
64
+ * trigger/interaction에서 추가된 user_context
65
+ */
66
+ additionalUserContext: ServiceInquiryUserContext | null;
67
+ /**
68
+ * 추가 user_context 교체 함수
69
+ */
70
+ setAdditionalUserContext: (
71
+ nextUserContext: ServiceInquiryUserContext | null,
72
+ ) => void;
73
+ /**
74
+ * 추가 user_context 병합 함수
75
+ */
76
+ appendUserContext: (
77
+ nextUserContext?: ServiceInquiryUserContext | null,
78
+ ) => void;
79
+ /**
80
+ * 추가 user_context 초기화 함수
81
+ */
82
+ clearAdditionalUserContext: () => void;
83
+ }
@@ -0,0 +1,4 @@
1
+ export type * from "./api";
2
+ export type * from "./form-context";
3
+ export type * from "./hooks";
4
+ export type * from "./props";
@@ -0,0 +1,171 @@
1
+ import type { ReactNode } from "react";
2
+ import type { SubmitHandler } from "react-hook-form";
3
+ import type { InputProps, InputTextAreaProps } from "@uniai-fe/uds-primitives";
4
+ import type { DialogTemplateOptions } from "../../modal/types";
5
+ import type { ServiceInquiryFormValues } from "./form-context";
6
+
7
+ /**
8
+ * Service Inquiry; 노출 필드 키
9
+ */
10
+ export type ServiceInquiryFieldKey = "farm_name" | "contact" | "text";
11
+
12
+ /**
13
+ * Service Inquiry Form; field 입력 모드
14
+ * @typedef {"editable" | "readonly"} ServiceInquiryFieldMode
15
+ */
16
+ export type ServiceInquiryFieldMode = "editable" | "readonly";
17
+
18
+ /**
19
+ * Service Inquiry Form; 공통 field 설정
20
+ * @property {string} [className] field wrapper className
21
+ * @property {ReactNode} [label] field label
22
+ * @property {ReactNode} [helper] field helper
23
+ * @property {boolean} [required] required 표시 여부
24
+ */
25
+ export interface ServiceInquiryFieldBaseProps {
26
+ /**
27
+ * field wrapper className
28
+ */
29
+ className?: string;
30
+ /**
31
+ * field label
32
+ */
33
+ label?: ReactNode;
34
+ /**
35
+ * field helper
36
+ */
37
+ helper?: ReactNode;
38
+ /**
39
+ * required 표시 여부
40
+ */
41
+ required?: boolean;
42
+ }
43
+
44
+ /**
45
+ * Service Inquiry Form; 단일 line input field 설정
46
+ * @property {"editable" | "readonly"} [mode] 서비스가 제어하는 field 입력 모드
47
+ * @property {string} [placeholder] input placeholder
48
+ * @property {InputProps} [inputProps] primitives Input.Base override
49
+ */
50
+ export interface ServiceInquiryInputFieldProps extends ServiceInquiryFieldBaseProps {
51
+ /**
52
+ * 서비스가 제어하는 field 입력 모드
53
+ */
54
+ mode?: ServiceInquiryFieldMode;
55
+ /**
56
+ * input placeholder
57
+ */
58
+ placeholder?: string;
59
+ /**
60
+ * primitives Input.Base override
61
+ */
62
+ inputProps?: InputProps;
63
+ }
64
+
65
+ /**
66
+ * Service Inquiry Form; textarea field 설정
67
+ * @property {string} [placeholder] textarea placeholder
68
+ * @property {InputTextAreaProps} [inputProps] primitives Input.TextArea override
69
+ */
70
+ export interface ServiceInquiryTextAreaFieldProps extends ServiceInquiryFieldBaseProps {
71
+ /**
72
+ * textarea placeholder
73
+ */
74
+ placeholder?: string;
75
+ /**
76
+ * primitives Input.TextArea override
77
+ */
78
+ inputProps?: InputTextAreaProps;
79
+ }
80
+
81
+ /**
82
+ * Service Inquiry Form; 문의 입력 폼 props
83
+ * @property {string} [className] form className
84
+ * @property {ServiceInquiryFieldKey[]} [visibleFields] 노출 필드 목록
85
+ * @property {ServiceInquiryInputFieldProps} [farmNameField] 농장명 필드 설정
86
+ * @property {ServiceInquiryInputFieldProps} [contactField] 연락처 필드 설정
87
+ * @property {ServiceInquiryTextAreaFieldProps} [textField] 문의 본문 필드 설정
88
+ * @property {SubmitHandler<ServiceInquiryFormValues>} props.onSubmit 문의 제출 핸들러
89
+ */
90
+ export interface ServiceInquiryFormProps {
91
+ /**
92
+ * form className
93
+ */
94
+ className?: string;
95
+ /**
96
+ * 노출 필드 목록
97
+ */
98
+ visibleFields?: ServiceInquiryFieldKey[];
99
+ /**
100
+ * 농장명 필드 설정
101
+ */
102
+ farmNameField?: ServiceInquiryInputFieldProps;
103
+ /**
104
+ * 연락처 필드 설정
105
+ */
106
+ contactField?: ServiceInquiryInputFieldProps;
107
+ /**
108
+ * 문의 본문 필드 설정
109
+ */
110
+ textField?: ServiceInquiryTextAreaFieldProps;
111
+ /**
112
+ * 문의 제출 핸들러
113
+ */
114
+ onSubmit: SubmitHandler<ServiceInquiryFormValues>;
115
+ }
116
+
117
+ /**
118
+ * Service Inquiry Modal; 문의 모달 preset 생성 옵션
119
+ * @property {string} stackKey modal stack key
120
+ * @property {ServiceInquiryFormProps} formProps 문의 form props
121
+ * @property {Partial<DialogTemplateOptions<ServiceInquiryFormValues>>} [dialogOptions] 문의 모달 preset 위에 덮어쓸 dialog option
122
+ */
123
+ // 변경 설명: DialogTemplateOptions를 거의 다시 적는 wrapper 대신, 모듈 고유 필드와 dialog override bag만 가진 preset 입력 타입으로 줄인다.
124
+ export interface ServiceInquiryCreateModalOptions {
125
+ /**
126
+ * modal stack key
127
+ */
128
+ stackKey: string;
129
+ /**
130
+ * 문의 form props
131
+ */
132
+ formProps: ServiceInquiryFormProps;
133
+ /**
134
+ * 문의 모달 preset 위에 덮어쓸 dialog option
135
+ */
136
+ dialogOptions?: Partial<DialogTemplateOptions<ServiceInquiryFormValues>>;
137
+ }
138
+
139
+ /**
140
+ * Service Inquiry Hook; 문의 모달 열기 훅 옵션
141
+ * @property {string} stackKey modal stack key
142
+ * @property {ServiceInquiryFormProps} formProps 문의 form props
143
+ * @property {Partial<DialogTemplateOptions<ServiceInquiryFormValues>>} [dialogOptions] 문의 모달 preset 위에 덮어쓸 dialog option
144
+ * @property {() => void} [onOpen] 모달 open 직전 콜백
145
+ */
146
+ export interface UseOpenServiceInquiryOptions extends ServiceInquiryCreateModalOptions {
147
+ /**
148
+ * 모달 open 직전 콜백
149
+ */
150
+ onOpen?: () => void;
151
+ }
152
+
153
+ /**
154
+ * Service Inquiry Hook; 문의 모달 열기 훅 반환값
155
+ * @property {() => void} openServiceInquiry 문의 모달 open 함수
156
+ */
157
+ export interface UseOpenServiceInquiryReturn {
158
+ /**
159
+ * 문의 모달 open 함수
160
+ */
161
+ openServiceInquiry: () => void;
162
+ }
163
+
164
+ /**
165
+ * Service Inquiry Open Button; 문의 모달 기본 버튼 props
166
+ * @property {string} stackKey modal stack key
167
+ * @property {ServiceInquiryFormProps} formProps 문의 form props
168
+ * @property {Partial<DialogTemplateOptions<ServiceInquiryFormValues>>} [dialogOptions] 문의 모달 preset 위에 덮어쓸 dialog option
169
+ * @property {() => void} [onOpen] 모달 open 직전 콜백
170
+ */
171
+ export interface ServiceInquiryOpenButtonProps extends UseOpenServiceInquiryOptions {}
@@ -0,0 +1,38 @@
1
+ import { createDialogModal } from "../../modal/components/dialog/Template";
2
+ import ServiceInquiryForm from "../components/Form";
3
+ import type {
4
+ ServiceInquiryCreateModalOptions,
5
+ ServiceInquiryFormValues,
6
+ } from "../types";
7
+
8
+ /**
9
+ * Service Inquiry Modal; 문의 form이 포함된 Dialog option 생성기
10
+ * @component
11
+ * @param {ServiceInquiryCreateModalOptions} options 문의 모달 preset 생성 옵션
12
+ * @param {string} options.stackKey modal stack key
13
+ * @param {ServiceInquiryFormProps} options.formProps 문의 form props
14
+ * @param {Partial<DialogTemplateOptions<ServiceInquiryFormValues>>} [options.dialogOptions] 문의 모달 preset 위에 덮어쓸 dialog option
15
+ * @example
16
+ * createServiceInquiryModal({
17
+ * stackKey: "service-inquiry",
18
+ * formProps: { onSubmit: values => console.info(values) },
19
+ * })
20
+ */
21
+ export function createServiceInquiryModal({
22
+ stackKey,
23
+ formProps,
24
+ dialogOptions,
25
+ }: ServiceInquiryCreateModalOptions) {
26
+ return createDialogModal<ServiceInquiryFormValues>({
27
+ ...dialogOptions,
28
+ stackKey,
29
+ title: dialogOptions?.title ?? "문의하기",
30
+ // 변경 설명: 문의 모달은 title과 안내 문구를 좌측 정렬로 읽히게 하기 위해 split header를 기본값으로 사용한다.
31
+ headerLayout: dialogOptions?.headerLayout ?? "split",
32
+ content: <ServiceInquiryForm {...formProps} />,
33
+ confirm: {
34
+ label: "문의 접수",
35
+ ...dialogOptions?.confirm,
36
+ },
37
+ });
38
+ }