@uniai-fe/uds-templates 0.4.2 → 0.4.4

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,17 @@ 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.useProvideContext`
63
+ - `ServiceInquiry.useUserContext`
64
+ - `ServiceInquiry.createModal`
65
+ - `ServiceInquiryFieldMode`
66
+ - `ServiceInquiryFormProps`
67
+ - `ServiceInquiryFormValues`
68
+ - `ServiceInquiryProvidedContext`
58
69
  - `/cctv`
59
70
  - `CCTV.Provider`
60
71
  - `CCTV.CamList.Container`
@@ -95,6 +106,11 @@ Next.js 서비스에서 primitives와 동일한 방식으로 **Raw TypeScript**
95
106
  - `/modal/**`
96
107
  - ui-legacy 스택 기반 모달 Provider/Root/Container + 템플릿(`Modal.Alert`, `Modal.Dialog`)
97
108
  - Storybook(`apps/design-storybook/src/stories/templates/modal`)에서 Alert/Confirm 케이스를 검증한다.
109
+ - `/service-inquiry/**`
110
+ - 문의 입력 전용 form, 기본 원형 `?` open button, 커스텀 trigger용 open hook, 페이지 context 등록 hook, request context 조립 hook, modal preset factory를 제공한다.
111
+ - submit transport, React Query mutation, Next.js route handler, 에러 피드백(`Modal.Alert`)은 서비스 앱이 소유한다.
112
+ - 모듈 내부 Jotai registry를 사용하므로, layout 고정 버튼 1개와 페이지별 context 등록 hook 조합으로 ready-to-use 구성이 가능하다.
113
+ - 로그인 후 `farm_name`, `contact`를 auto-fill + readonly로 보여야 할 때는 `formContextOptions.defaultValues`와 `farmNameField.mode`, `contactField.mode`를 함께 전달한다.
98
114
  - `/weather/**`
99
115
  - page-frame header utility에 결합되는 weather header 템플릿과 weather data hook/mock 도구를 제공한다.
100
116
  - `/cctv/**`
@@ -104,6 +120,17 @@ Next.js 서비스에서 primitives와 동일한 방식으로 **Raw TypeScript**
104
120
 
105
121
  각 템플릿의 상세한 범위와 의사결정은 `CONTEXT-*.md` 문서에서 관리합니다.
106
122
 
123
+ ### Service Inquiry 도입 흐름
124
+
125
+ 1. 서비스 앱이 `react-hook-form`으로 `defaultValues`와 `onSubmit`을 준비한다.
126
+ 2. layout 고정 버튼은 `ServiceInquiry.useUserContext()`로 모듈 내부 registry 기반 `requestContext`를 읽는다.
127
+ 3. 각 페이지/폼은 `ServiceInquiry.useProvideContext({ labels, pagePath, userContext })`로 `ServiceInquiryProvidedContext`를 등록한다.
128
+ 4. 기본 버튼은 `ServiceInquiry.OpenButton`, 커스텀 버튼은 `ServiceInquiry.useOpen`으로 모달 open을 연결한다.
129
+ 5. modal footer confirm이 `ServiceInquiry.Form` submit 진입을 담당한다.
130
+ 6. 실제 submit은 서비스 앱의 `useMutation + Next.js route handler + Modal.Alert` 조합으로 처리한다.
131
+
132
+ `service-inquiry`는 구조와 request context까지만 제공하고, 네트워크 상태/재시도/성공·실패 피드백은 서비스 앱이 소유합니다.
133
+
107
134
  ### 회원가입 Step 구조
108
135
 
109
136
  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.4",
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,145 @@
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
+ )}
47
+ width="full"
48
+ headerProps={{
49
+ required: farmNameField?.required,
50
+ ...(typeof farmNameField?.label === "string"
51
+ ? { label: farmNameField.label }
52
+ : typeof farmNameField?.label === "number"
53
+ ? { label: String(farmNameField.label) }
54
+ : {
55
+ labelJsx:
56
+ typeof farmNameField?.label === "undefined"
57
+ ? "농장명"
58
+ : farmNameField.label,
59
+ }),
60
+ }}
61
+ footer={farmNameField?.helper}
62
+ >
63
+ <Input.Base
64
+ type="text"
65
+ block={true}
66
+ readOnly={farmNameField?.mode === "readonly"}
67
+ placeholder={farmNameField?.placeholder ?? "농장명 입력"}
68
+ register={form.register("farm_name")}
69
+ />
70
+ </Form.Field.Template>
71
+ ) : null}
72
+
73
+ {(visibleFields?.includes("contact") ?? true) ? (
74
+ <Form.Field.Template
75
+ className={clsx(
76
+ "service-inquiry-field",
77
+ "service-inquiry-field-contact",
78
+ )}
79
+ width="full"
80
+ headerProps={{
81
+ required: contactField?.required ?? true,
82
+ ...(typeof contactField?.label === "string"
83
+ ? { label: contactField.label }
84
+ : typeof contactField?.label === "number"
85
+ ? { label: String(contactField.label) }
86
+ : {
87
+ labelJsx:
88
+ typeof contactField?.label === "undefined"
89
+ ? "연락처"
90
+ : contactField.label,
91
+ }),
92
+ }}
93
+ footer={contactField?.helper}
94
+ >
95
+ <Input.Base
96
+ type="text"
97
+ block={true}
98
+ readOnly={contactField?.mode === "readonly"}
99
+ placeholder={
100
+ contactField?.placeholder ?? "이메일 또는 전화번호 입력"
101
+ }
102
+ register={form.register("contact")}
103
+ />
104
+ </Form.Field.Template>
105
+ ) : null}
106
+
107
+ {(visibleFields?.includes("text") ?? true) ? (
108
+ <Form.Field.Template
109
+ className={clsx(
110
+ "service-inquiry-field",
111
+ "service-inquiry-field-text",
112
+ )}
113
+ width="full"
114
+ headerProps={{
115
+ required: textField?.required ?? true,
116
+ ...(typeof textField?.label === "string"
117
+ ? { label: textField.label }
118
+ : typeof textField?.label === "number"
119
+ ? { label: String(textField.label) }
120
+ : {
121
+ labelJsx:
122
+ typeof textField?.label === "undefined"
123
+ ? "문의 내용"
124
+ : textField.label,
125
+ }),
126
+ }}
127
+ footer={textField?.helper}
128
+ >
129
+ <Input.TextArea
130
+ block={true}
131
+ placeholder={
132
+ textField?.placeholder ?? "문의 내용을 자세히 입력해 주세요."
133
+ }
134
+ height={160}
135
+ length={10000}
136
+ register={form.register("text")}
137
+ />
138
+ </Form.Field.Template>
139
+ ) : null}
140
+ </div>
141
+ </form>
142
+ );
143
+ };
144
+
145
+ export default ServiceInquiryForm;
@@ -0,0 +1,46 @@
1
+ "use client";
2
+
3
+ import { useOpenServiceInquiry } from "../hooks";
4
+ import type { UseOpenServiceInquiryOptions } from "../types";
5
+
6
+ /**
7
+ * Service Inquiry Open Button; 문의 모달 trigger adapter
8
+ * @component
9
+ * @param {UseOpenServiceInquiryOptions} 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
+ }: UseOpenServiceInquiryOptions) => {
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,3 @@
1
+ export * from "./useOpen";
2
+ export * from "./useProvideContext";
3
+ export * from "./useUserContext";
@@ -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,49 @@
1
+ "use client";
2
+
3
+ import { useEffect, useId } from "react";
4
+ import { useSetAtom } from "jotai";
5
+ import { serviceInquiryProvidedContextRegistryAtom } from "../jotai";
6
+ import type { ServiceInquiryProvidedContext } from "../types";
7
+
8
+ /**
9
+ * Service Inquiry Hook; 페이지별 문의 context 등록 Hook
10
+ * @hook
11
+ * @param {ServiceInquiryProvidedContext | null} context 페이지 또는 폼이 등록할 문의 context
12
+ * @example
13
+ * useProvideServiceInquiryContext({
14
+ * labels: ["orders"],
15
+ * userContext: { order_id: selectedOrderId },
16
+ * });
17
+ */
18
+ export function useProvideServiceInquiryContext(
19
+ context: ServiceInquiryProvidedContext | null,
20
+ ): void {
21
+ const registrationId = useId();
22
+ const setProvidedContextRegistry = useSetAtom(
23
+ serviceInquiryProvidedContextRegistryAtom,
24
+ );
25
+
26
+ useEffect(() => {
27
+ setProvidedContextRegistry(currentRegistry => {
28
+ const nextRegistry = { ...currentRegistry };
29
+
30
+ if (context) {
31
+ nextRegistry[registrationId] = context;
32
+ } else {
33
+ delete nextRegistry[registrationId];
34
+ }
35
+
36
+ return nextRegistry;
37
+ });
38
+
39
+ return () => {
40
+ setProvidedContextRegistry(currentRegistry => {
41
+ const nextRegistry = { ...currentRegistry };
42
+
43
+ delete nextRegistry[registrationId];
44
+
45
+ return nextRegistry;
46
+ });
47
+ };
48
+ }, [context, registrationId, setProvidedContextRegistry]);
49
+ }
@@ -0,0 +1,57 @@
1
+ "use client";
2
+
3
+ import { useAtom, useAtomValue } from "jotai";
4
+ import type {
5
+ ServiceInquiryUserContext,
6
+ UseServiceInquiryUserContextReturn,
7
+ } from "../types";
8
+ import {
9
+ serviceInquiryAdditionalUserContextAtom,
10
+ serviceInquiryRequestContextAtom,
11
+ } from "../jotai";
12
+
13
+ /**
14
+ * Service Inquiry Hook; request context 제어 Hook
15
+ * @hook
16
+ * @returns {UseServiceInquiryUserContextReturn} request context 및 추가 user_context 제어 함수
17
+ * @example
18
+ * const inquiryContext = useServiceInquiryUserContext();
19
+ */
20
+ export function useServiceInquiryUserContext(): UseServiceInquiryUserContextReturn {
21
+ /** 1) form init — request context snapshot과 trigger 추가 context를 atom 기반으로 읽는다. */
22
+ const requestContext = useAtomValue(serviceInquiryRequestContextAtom);
23
+ const [additionalUserContext, setAdditionalUserContext] = useAtom(
24
+ serviceInquiryAdditionalUserContextAtom,
25
+ );
26
+
27
+ /** 2) submit/액션 — trigger별 추가 context를 append/reset 할 수 있게 연다. */
28
+ const setNextAdditionalUserContext: UseServiceInquiryUserContextReturn["setAdditionalUserContext"] =
29
+ nextUserContext => {
30
+ setAdditionalUserContext(nextUserContext);
31
+ };
32
+
33
+ const appendUserContext: UseServiceInquiryUserContextReturn["appendUserContext"] =
34
+ (nextUserContext?: ServiceInquiryUserContext | null) => {
35
+ if (!nextUserContext) {
36
+ return;
37
+ }
38
+
39
+ setAdditionalUserContext(currentUserContext => ({
40
+ ...(currentUserContext ?? {}),
41
+ ...nextUserContext,
42
+ }));
43
+ };
44
+
45
+ const clearAdditionalUserContext: UseServiceInquiryUserContextReturn["clearAdditionalUserContext"] =
46
+ () => {
47
+ setAdditionalUserContext(null);
48
+ };
49
+
50
+ return {
51
+ requestContext,
52
+ additionalUserContext,
53
+ setAdditionalUserContext: setNextAdditionalUserContext,
54
+ appendUserContext,
55
+ clearAdditionalUserContext,
56
+ };
57
+ }
@@ -0,0 +1 @@
1
+ @use "./styles/index.scss";
@@ -0,0 +1,32 @@
1
+ import "./index.scss";
2
+ import ServiceInquiryForm from "./components/Form";
3
+ import ServiceInquiryOpenButton from "./components/OpenButton";
4
+ import {
5
+ useOpenServiceInquiry,
6
+ useProvideServiceInquiryContext,
7
+ useServiceInquiryUserContext,
8
+ } from "./hooks";
9
+ import { createServiceInquiryModal } from "./utils/modal-option";
10
+
11
+ /**
12
+ * Service Inquiry; 문의 form + modal option 엔트리
13
+ * - 변경 설명: Step 2에서 Form과 Modal.Dialog 기반 factory를 첫 runtime 조각으로 연다.
14
+ */
15
+ export const ServiceInquiry = {
16
+ Form: ServiceInquiryForm,
17
+ OpenButton: ServiceInquiryOpenButton,
18
+ useOpen: useOpenServiceInquiry,
19
+ useProvideContext: useProvideServiceInquiryContext,
20
+ useUserContext: useServiceInquiryUserContext,
21
+ createModal: createServiceInquiryModal,
22
+ };
23
+
24
+ export {
25
+ ServiceInquiryForm,
26
+ ServiceInquiryOpenButton,
27
+ useOpenServiceInquiry,
28
+ useProvideServiceInquiryContext,
29
+ useServiceInquiryUserContext,
30
+ createServiceInquiryModal,
31
+ };
32
+ export type * from "./types";
@@ -0,0 +1,88 @@
1
+ "use client";
2
+
3
+ import { atom } from "jotai";
4
+ import type {
5
+ ServiceInquiryRequestContext,
6
+ ServiceInquiryProvidedContext,
7
+ ServiceInquiryUserContext,
8
+ } from "../types";
9
+
10
+ /**
11
+ * Service Inquiry State; 페이지별 등록 context registry
12
+ * @state
13
+ */
14
+ export const serviceInquiryProvidedContextRegistryAtom = atom<
15
+ Record<string, ServiceInquiryProvidedContext>
16
+ >({});
17
+
18
+ /**
19
+ * Service Inquiry State; trigger/interaction 추가 user_context
20
+ * @state
21
+ */
22
+ export const serviceInquiryAdditionalUserContextAtom =
23
+ atom<ServiceInquiryUserContext | null>(null);
24
+
25
+ /**
26
+ * Service Inquiry State; registry 병합 context
27
+ * @state
28
+ */
29
+ export const serviceInquiryProvidedContextAtom =
30
+ atom<ServiceInquiryProvidedContext>(get => {
31
+ const providedContexts = Object.values(
32
+ get(serviceInquiryProvidedContextRegistryAtom),
33
+ );
34
+
35
+ return {
36
+ labels: Array.from(
37
+ new Set(
38
+ providedContexts.flatMap(
39
+ providedContext => providedContext.labels ?? [],
40
+ ),
41
+ ),
42
+ ),
43
+ pagePath:
44
+ [...providedContexts].reverse().find(providedContext => {
45
+ return (
46
+ typeof providedContext.pagePath === "string" &&
47
+ providedContext.pagePath.length > 0
48
+ );
49
+ })?.pagePath ?? "",
50
+ userContext: providedContexts.reduce<ServiceInquiryUserContext | null>(
51
+ (currentUserContext, providedContext) => {
52
+ if (!providedContext.userContext) {
53
+ return currentUserContext;
54
+ }
55
+
56
+ return {
57
+ ...(currentUserContext ?? {}),
58
+ ...providedContext.userContext,
59
+ };
60
+ },
61
+ null,
62
+ ),
63
+ };
64
+ });
65
+
66
+ /**
67
+ * Service Inquiry State; 최종 request context
68
+ * @state
69
+ */
70
+ export const serviceInquiryRequestContextAtom =
71
+ atom<ServiceInquiryRequestContext>(get => {
72
+ const providedContext = get(serviceInquiryProvidedContextAtom);
73
+ const additionalUserContext = get(serviceInquiryAdditionalUserContextAtom);
74
+
75
+ return {
76
+ labels: providedContext.labels ?? [],
77
+ page_path:
78
+ providedContext.pagePath ||
79
+ (typeof window === "undefined" ? "" : window.location.pathname),
80
+ user_context:
81
+ providedContext.userContext || additionalUserContext
82
+ ? {
83
+ ...(providedContext.userContext ?? {}),
84
+ ...(additionalUserContext ?? {}),
85
+ }
86
+ : null,
87
+ };
88
+ });
@@ -0,0 +1 @@
1
+ export * from "./context";
@@ -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,60 @@
1
+ import type {
2
+ ServiceInquiryRequestContext,
3
+ ServiceInquiryUserContext,
4
+ } from "./form-context";
5
+
6
+ /**
7
+ * Service Inquiry Hook; 모듈 내부 공유 context 값
8
+ * @property {string[]} [labels] 문의 request에 포함할 labels
9
+ * @property {string} [pagePath] 문의를 연 페이지 경로
10
+ * @property {ServiceInquiryUserContext | null} [userContext] 페이지/폼이 제공하는 추가 user_context
11
+ */
12
+ export interface ServiceInquiryProvidedContext {
13
+ /**
14
+ * 문의 request에 포함할 labels
15
+ */
16
+ labels?: string[];
17
+ /**
18
+ * 문의를 연 페이지 경로
19
+ */
20
+ pagePath?: string;
21
+ /**
22
+ * 페이지/폼이 제공하는 추가 user_context
23
+ */
24
+ userContext?: ServiceInquiryUserContext | null;
25
+ }
26
+
27
+ /**
28
+ * Service Inquiry Hook; user_context 훅 반환값
29
+ * @property {ServiceInquiryRequestContext} requestContext form submit에 바로 사용할 request context
30
+ * @property {ServiceInquiryUserContext | null} additionalUserContext trigger/interaction에서 추가된 user_context
31
+ * @property {(nextUserContext: ServiceInquiryUserContext | null) => void} setAdditionalUserContext 추가 user_context 교체 함수
32
+ * @property {(nextUserContext?: ServiceInquiryUserContext | null) => void} appendUserContext 추가 user_context 병합 함수
33
+ * @property {() => void} clearAdditionalUserContext 추가 user_context 초기화 함수
34
+ */
35
+ export interface UseServiceInquiryUserContextReturn {
36
+ /**
37
+ * form submit에 바로 사용할 request context
38
+ */
39
+ requestContext: ServiceInquiryRequestContext;
40
+ /**
41
+ * trigger/interaction에서 추가된 user_context
42
+ */
43
+ additionalUserContext: ServiceInquiryUserContext | null;
44
+ /**
45
+ * 추가 user_context 교체 함수
46
+ */
47
+ setAdditionalUserContext: (
48
+ nextUserContext: ServiceInquiryUserContext | null,
49
+ ) => void;
50
+ /**
51
+ * 추가 user_context 병합 함수
52
+ */
53
+ appendUserContext: (
54
+ nextUserContext?: ServiceInquiryUserContext | null,
55
+ ) => void;
56
+ /**
57
+ * 추가 user_context 초기화 함수
58
+ */
59
+ clearAdditionalUserContext: () => void;
60
+ }
@@ -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,146 @@
1
+ import type { ReactNode } from "react";
2
+ import type { SubmitHandler } from "react-hook-form";
3
+ import type { DialogTemplateOptions } from "../../modal/types";
4
+ import type { ServiceInquiryFormValues } from "./form-context";
5
+
6
+ /**
7
+ * Service Inquiry; 노출 필드 키
8
+ */
9
+ export type ServiceInquiryFieldKey = "farm_name" | "contact" | "text";
10
+
11
+ /**
12
+ * Service Inquiry Form; field 입력 모드
13
+ * @typedef {"editable" | "readonly"} ServiceInquiryFieldMode
14
+ */
15
+ export type ServiceInquiryFieldMode = "editable" | "readonly";
16
+
17
+ /**
18
+ * Service Inquiry Form; 공통 field 설정
19
+ * @property {ReactNode} [label] field label
20
+ * @property {ReactNode} [helper] field helper
21
+ * @property {boolean} [required] required 표시 여부
22
+ */
23
+ export interface ServiceInquiryFieldBaseProps {
24
+ /**
25
+ * field label
26
+ */
27
+ label?: ReactNode;
28
+ /**
29
+ * field helper
30
+ */
31
+ helper?: ReactNode;
32
+ /**
33
+ * required 표시 여부
34
+ */
35
+ required?: boolean;
36
+ }
37
+
38
+ /**
39
+ * Service Inquiry Form; 단일 line input field 설정
40
+ * @property {"editable" | "readonly"} [mode] 서비스가 제어하는 field 입력 모드
41
+ * @property {string} [placeholder] input placeholder
42
+ */
43
+ export interface ServiceInquiryInputFieldProps extends ServiceInquiryFieldBaseProps {
44
+ /**
45
+ * 서비스가 제어하는 field 입력 모드
46
+ */
47
+ mode?: ServiceInquiryFieldMode;
48
+ /**
49
+ * input placeholder
50
+ */
51
+ placeholder?: string;
52
+ }
53
+
54
+ /**
55
+ * Service Inquiry Form; textarea field 설정
56
+ * @property {string} [placeholder] textarea placeholder
57
+ */
58
+ export interface ServiceInquiryTextAreaFieldProps extends ServiceInquiryFieldBaseProps {
59
+ /**
60
+ * textarea placeholder
61
+ */
62
+ placeholder?: string;
63
+ }
64
+
65
+ /**
66
+ * Service Inquiry Form; 문의 입력 폼 props
67
+ * @property {string} [className] form className
68
+ * @property {ServiceInquiryFieldKey[]} [visibleFields] 노출 필드 목록
69
+ * @property {ServiceInquiryInputFieldProps} [farmNameField] 농장명 필드 설정
70
+ * @property {ServiceInquiryInputFieldProps} [contactField] 연락처 필드 설정
71
+ * @property {ServiceInquiryTextAreaFieldProps} [textField] 문의 본문 필드 설정
72
+ * @property {SubmitHandler<ServiceInquiryFormValues>} props.onSubmit 문의 제출 핸들러
73
+ */
74
+ export interface ServiceInquiryFormProps {
75
+ /**
76
+ * form className
77
+ */
78
+ className?: string;
79
+ /**
80
+ * 노출 필드 목록
81
+ */
82
+ visibleFields?: ServiceInquiryFieldKey[];
83
+ /**
84
+ * 농장명 필드 설정
85
+ */
86
+ farmNameField?: ServiceInquiryInputFieldProps;
87
+ /**
88
+ * 연락처 필드 설정
89
+ */
90
+ contactField?: ServiceInquiryInputFieldProps;
91
+ /**
92
+ * 문의 본문 필드 설정
93
+ */
94
+ textField?: ServiceInquiryTextAreaFieldProps;
95
+ /**
96
+ * 문의 제출 핸들러
97
+ */
98
+ onSubmit: SubmitHandler<ServiceInquiryFormValues>;
99
+ }
100
+
101
+ /**
102
+ * Service Inquiry Modal; 문의 모달 preset 생성 옵션
103
+ * @property {string} stackKey modal stack key
104
+ * @property {ServiceInquiryFormProps} formProps 문의 form props
105
+ * @property {Partial<DialogTemplateOptions<ServiceInquiryFormValues>>} [dialogOptions] 문의 모달 preset 위에 덮어쓸 dialog option
106
+ */
107
+ // 변경 설명: DialogTemplateOptions를 거의 다시 적는 wrapper 대신, 모듈 고유 필드와 dialog override bag만 가진 preset 입력 타입으로 줄인다.
108
+ export interface ServiceInquiryCreateModalOptions {
109
+ /**
110
+ * modal stack key
111
+ */
112
+ stackKey: string;
113
+ /**
114
+ * 문의 form props
115
+ */
116
+ formProps: ServiceInquiryFormProps;
117
+ /**
118
+ * 문의 모달 preset 위에 덮어쓸 dialog option
119
+ */
120
+ dialogOptions?: Partial<DialogTemplateOptions<ServiceInquiryFormValues>>;
121
+ }
122
+
123
+ /**
124
+ * Service Inquiry Hook; 문의 모달 열기 훅 옵션
125
+ * @property {string} stackKey modal stack key
126
+ * @property {ServiceInquiryFormProps} formProps 문의 form props
127
+ * @property {Partial<DialogTemplateOptions<ServiceInquiryFormValues>>} [dialogOptions] 문의 모달 preset 위에 덮어쓸 dialog option
128
+ * @property {() => void} [onOpen] 모달 open 직전 콜백
129
+ */
130
+ export interface UseOpenServiceInquiryOptions extends ServiceInquiryCreateModalOptions {
131
+ /**
132
+ * 모달 open 직전 콜백
133
+ */
134
+ onOpen?: () => void;
135
+ }
136
+
137
+ /**
138
+ * Service Inquiry Hook; 문의 모달 열기 훅 반환값
139
+ * @property {() => void} openServiceInquiry 문의 모달 open 함수
140
+ */
141
+ export interface UseOpenServiceInquiryReturn {
142
+ /**
143
+ * 문의 모달 open 함수
144
+ */
145
+ openServiceInquiry: () => void;
146
+ }
@@ -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
+ }