Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/api/admin/customerService/inquiry.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ export const getAllInquiries = async (
}
};

export const getInquiriesPreview = async () => {
const response = await httpClient.get<ApiAdminInquiry>(`/inquiry/preview`);
return response.data.data;
};

export const getInquiryDetail = async (id: string) => {
try {
const response = await httpClient.get<ApiAdminInquiryDetail>(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
import * as S from './InquiresPreview.styled';
import { useGetAllInquiries } from '../../../../hooks/admin/useGetAllInquiries';
import Avatar from '../../../common/avatar/Avatar';
import { ADMIN_ROUTE } from '../../../../constants/routes';
import arrow_right from '../../../../assets/ArrowRight.svg';
import Spinner from '../../../user/mypage/Spinner';
import { AdminInquiryChangeSearchParams } from '../../../../models/inquiry';
import { useGetInquiriesPreview } from '../../../../hooks/admin/useGetInquiriesPreview';

const InquiresPreview = () => {
const childSearchParams: AdminInquiryChangeSearchParams = {
userId: '',
startDate: '',
endDate: '',
};
const { allInquiriesData, isLoading, isFetching } =
useGetAllInquiries(childSearchParams);
const { allInquiriesPreviewData, isLoading, isFetching } =
useGetInquiriesPreview();

if (isLoading || isFetching) {
return (
Expand All @@ -23,13 +17,13 @@ const InquiresPreview = () => {
);
}

if (!allInquiriesData || allInquiriesData.length === 0) {
if (!allInquiriesPreviewData || allInquiriesPreviewData.length === 0) {
return <S.Container>등록된 문의가 없습니다.</S.Container>;
}

return (
<S.Container>
{allInquiriesData?.map((inquiry) => (
{allInquiriesPreviewData.map((inquiry) => (
<S.Wrapper key={inquiry.id}>
<S.Content>
{/* <Link to={`${ADMIN_ROUTE.}`} */}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const NoticePreview = () => {
}

if (!noticeData) {
return <S.Container>공지사힝이 없습니다.</S.Container>;
return <S.Container>공지사항이 없습니다.</S.Container>;
}

return (
Expand Down
1 change: 0 additions & 1 deletion src/components/user/mypage/ContentTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ export default function ContentTab({ filter, $justifyContent }: ContentProps) {
to={filter.url}
onClick={() => handleChangeId(filter.id as number)}
>
{' '}
<S.WrapperTitle $selected={filter?.id === filterId}>
<S.FilterTitle>{filter.title}</S.FilterTitle>
</S.WrapperTitle>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,34 @@ export const InputContainer = styled.div`
flex-direction: column;
`;

export const InputStyle = styled.input<{ type?: string }>`
export const InputWrapper = styled.div`
display: flex;
align-items: center;
gap: 2px;
`;

export const UnitText = styled.span`
font-size: ${({ theme }) => theme.heading.xsSmall.fontSize};
color: ${({ theme }) => theme.color.deepGrey};
white-space: nowrap;
`;

export const InputStyle = styled.input<{ type?: string; name?: string }>`
padding: 10px;
border: 1px solid ${({ theme }) => theme.color.border};
border-radius: ${({ theme }) => theme.borderRadius.primary};
font-size: ${({ theme }) => theme.heading.small.fontSize};

${({ type }) => {
${({ type, name }) => {
switch (type) {
case 'text':
if (name === 'maxVolunteers' || name === 'duration') {
return css`
width: 60px;
text-align: right;
border-radius: 13px;
`;
}
return css`
width: 100%;
border-radius: 13px;
Expand All @@ -40,19 +59,20 @@ export const InputStyle = styled.input<{ type?: string }>`
}}
`;

export const InputInfoStyle = styled.input<{ type?: string }>`
export const InputInfoStyle = styled.input<{ type?: string; name?: string }>`
font-size: ${({ theme }) => theme.heading.small.fontSize};
border: none;

${({ type }) => {
${({ type, name }) => {
switch (type) {
case 'text':
return css`
width: 220px;
text-align: left;
padding-left: 1px;
`;
case 'number':
if (name === 'maxVolunteers' || name === 'duration') {
return css`
width: 30px;
text-align: right;
padding-left: 1px;
`;
}
return css`
width: 220px;
text-align: left;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ import * as S from './InputComponent.styled';
import MdEditorInput from '../editor/MarkdownEditor';

type InputProps = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
control: Control<any>;
name: string;
placeholder?: string;
errors?: FieldErrors;
type?: 'text' | 'date' | 'textarea' | 'number' | string;
indexInfo?: string;
unit?: string;
min?: number;
};

const Input = ({
Expand All @@ -18,25 +21,70 @@ const Input = ({
placeholder,
type,
indexInfo,
unit,
min = 0,
}: InputProps) => {
const { field } = useController({
control,
name,
});
const hasError = Boolean(errors?.[name]);

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;

// 숫자 입력 필드인 경우 숫자만 허용
if (name === 'maxVolunteers' || name === 'duration') {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

스키마와 입력 검증의 일관성을 확인해주세요

maxVolunteersduration 필드가 텍스트 입력으로 변경되었는데, 프로젝트 상수 파일의 검증 스키마가 이 변경사항과 일치하는지 확인이 필요합니다.

다음 스크립트를 실행하여 스키마 일관성을 확인해주세요:


🏁 Script executed:

#!/bin/bash
# 스키마에서 maxVolunteers와 duration 필드의 타입 확인
rg -A 10 "maxVolunteers.*z\." src/constants/user/projectConstants.ts
rg -A 10 "duration.*z\." src/constants/user/projectConstants.ts

Length of output: 136


다음 스크립트로 projectConstants.ts 내 검증 스키마 정의와 maxVolunteers·duration 타입을 다시 확인해주세요:


🏁 Script executed:

#!/bin/bash
# Zod 스키마 정의 위치 확인
rg -n "z.object" -A 20 src/constants/user/projectConstants.ts || true

# 각 필드 타입 확인
rg -n "maxVolunteers" -A 5 src/constants/user/projectConstants.ts || true
rg -n "duration" -A 5 src/constants/user/projectConstants.ts || true

Length of output: 2145


duration 필드 스키마 타입 불일치 보강 필요

프로젝트 상수(src/constants/user/projectConstants.ts)의 createProjectScheme에서:

  • maxVolunteers
    • z.number — UI에서 text 입력 후 숫자로 변환 → 검증 일치
  • duration
    • z.string — UI에서 text 입력만 검증(refine) → 반환 타입은 string

두 필드를 모두 숫자형으로 다룰 예정이라면 durationz.number로 변경하거나, 반대로 maxVolunteersz.string 기반 검증으로 통일해야 합니다.

수정 예시:

--- a/src/constants/user/projectConstants.ts
+++ b/src/constants/user/projectConstants.ts
@@ -95,7 +95,7 @@ export const createProjectScheme = z.object({
-  duration: z
-    .string({ message: '예상 기간을 입력해주세요.' })
-    .min(1, { message: '예상 기간을 입력해주세요.' })
-    .refine((val) => {
-      const num = Number(val);
-      return !isNaN(num) && num > 0;
-    }, { message: '유효한 기간을 입력해주세요.' }),
+  duration: z
+    .number({ message: '예상 기간을 입력해주세요.' })
+    .min(1, { message: '예상 기간을 입력해주세요.' }),

위와 같이 duration을 z.number로 변경해 일관성을 맞춰주세요.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (name === 'maxVolunteers' || name === 'duration') {
export const createProjectScheme = z.object({
// ... other schema fields ...
- duration: z
- .string({ message: '예상 기간을 입력해주세요.' })
- .min(1, { message: '예상 기간을 입력해주세요.' })
- .refine((val) => {
- const num = Number(val);
- return !isNaN(num) && num > 0;
- }, { message: '유효한 기간을 입력해주세요.' }),
+ duration: z
+ .number({ message: '예상 기간을 입력해주세요.' })
+ .min(1, { message: '예상 기간을 입력해주세요.' }),
maxVolunteers: z
.number({ message: '모집 인원을 입력해주세요.' })
.min(1, { message: '모집 인원은 1명이상이어야 합니다.' }),
// ... other schema fields ...
});
🤖 Prompt for AI Agents
In src/components/user/projectFormComponents/inputComponent/InputComponent.tsx
at line 37, the condition checks for 'duration' and 'maxVolunteers' fields, but
the schema type for 'duration' in src/constants/user/projectConstants.ts's
createProjectScheme is z.string while 'maxVolunteers' is z.number. To maintain
consistency and handle both as numbers, update the createProjectScheme to change
the 'duration' field type from z.string to z.number, ensuring the validation and
UI handling align with numeric types for both fields.

// 숫자가 아닌 문자 제거
const numericValue = value.replace(/[^0-9]/g, '');
if (numericValue !== value) {
e.target.value = numericValue;
}

// 음수 방지 (0보다 작은 값은 허용하지 않음)
const numValue = Number(numericValue);
if (numValue < min) {
return;
}
}

field.onChange(e);
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

입력 검증 로직을 개선해주세요

숫자 입력 검증 로직이 잘 구현되었지만, e.target.value를 직접 수정한 후 field.onChange(e)를 호출하는 방식은 일관성 문제를 야기할 수 있습니다.

더 안정적인 구현을 위해 다음과 같이 수정하는 것을 권장합니다:

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
-  const value = e.target.value;
+  let value = e.target.value;

  // 숫자 입력 필드인 경우 숫자만 허용
  if (name === 'maxVolunteers' || name === 'duration') {
    // 숫자가 아닌 문자 제거
-    const numericValue = value.replace(/[^0-9]/g, '');
-    if (numericValue !== value) {
-      e.target.value = numericValue;
-    }
+    value = value.replace(/[^0-9]/g, '');

    // 음수 방지 (0보다 작은 값은 허용하지 않음)
-    const numValue = Number(numericValue);
+    const numValue = Number(value);
    if (numValue < min) {
      return;
    }
  }

-  field.onChange(e);
+  field.onChange(value);
};
🤖 Prompt for AI Agents
In src/components/user/projectFormComponents/inputComponent/InputComponent.tsx
around lines 33 to 52, avoid directly modifying e.target.value before calling
field.onChange(e) as it can cause inconsistencies. Instead, extract and sanitize
the input value first, then call field.onChange with a synthetic event or
directly pass the sanitized value to update the form state. This ensures
consistent and reliable input handling without mutating the original event
object.


const renderInput = () => {
if (indexInfo) {
return (
<S.InputInfoStyle {...field} type={type} placeholder={placeholder} />
<S.InputWrapper>
<S.InputInfoStyle
{...field}
type={type}
placeholder={placeholder}
onChange={handleInputChange}
min={min}
name={name}
/>
{unit && <S.UnitText>{unit}</S.UnitText>}
</S.InputWrapper>
);
}

if (name === 'markdownEditor') {
return <MdEditorInput field={{ ...field }} />;
}

return <S.InputStyle {...field} type={type} placeholder={placeholder} />;
return (
<S.InputWrapper>
<S.InputStyle
{...field}
type={type}
placeholder={placeholder}
onChange={handleInputChange}
min={min}
name={name}
/>
{unit && <S.UnitText>{unit}</S.UnitText>}
</S.InputWrapper>
);
};

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ export const InfoLabel = styled.label`
color: #333;
`;

export const LabelDescription = styled.span`
font-size: ${({ theme }) => theme.heading.xsSmall.fontSize};
color: ${({ theme }) => theme.color.grey};
font-weight: normal;
margin-left: 8px;
`;

export const welcomeSprout = styled.p`
font-size: 1rem;
font-weight: bold;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,23 @@ const ProjectInformationInput = ({
return (
<>
{PROJECT_DATA.map((input, index) => (
<>
<S.InfoRow key={index}>
<S.InfoLabel htmlFor={input.name}>{input.label}</S.InfoLabel>
<Input
indexInfo={input.id}
control={control}
errors={errors}
name={input.name}
type={input.type}
placeholder={input.placeholder}
/>
{input.type === 'checkbox' && (
<S.welcomeSprout>
새싹 멤버를 환영한다면 체크를 눌러주세요!!
</S.welcomeSprout>
)}
</S.InfoRow>
</>
<S.InfoRow key={index}>
<S.InfoLabel htmlFor={input.name}>{input.label}</S.InfoLabel>
<Input
indexInfo={input.id}
control={control}
errors={errors}
name={input.name}
type={input.type}
unit={input.unit}
min={1}
/>
{input.type === 'checkbox' && (
<S.welcomeSprout>
새싹 멤버를 환영한다면 체크를 눌러주세요!!
</S.welcomeSprout>
)}
</S.InfoRow>
))}

<S.InfoRow>
Expand All @@ -59,7 +58,12 @@ const ProjectInformationInput = ({
/>

<S.InfoRow>
<S.InfoLabel htmlFor='position'>모집 분야</S.InfoLabel>
<S.InfoLabel htmlFor='position'>
모집 분야
<S.welcomeSprout>
전체를 클릭하면 다른 카테고리가 해제됩니다 !
</S.welcomeSprout>
</S.InfoLabel>
</S.InfoRow>
<MozipCategoryComponent
name='position'
Expand Down
36 changes: 25 additions & 11 deletions src/constants/user/projectConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,24 @@ export const PROJECT_DATA = [
id: '1',
name: 'maxVolunteers',
label: '모집 인원',
type: 'number',
placeholder: '모집 인원을 입력하세요. (ex. 5)',
type: 'text',
unit: '',
},
{
id: '2',
name: 'startDatePre',
label: '시작 예정',
type: 'text',
placeholder: 'YYYY-MM-DD',
unit: '',
},

{
id: '3',
name: 'duration',
label: '예상 기간',
type: 'number',
placeholder: '예상 기간을 입력하세요. (ex. 5)',
type: 'text',
unit: '개월',
},

{
Expand All @@ -32,6 +33,7 @@ export const PROJECT_DATA = [
label: '새싹 여부',
type: 'checkbox',
placeholder: '',
unit: '',
},
] as const;

Expand Down Expand Up @@ -74,20 +76,32 @@ export const createProjectScheme = z.object({
.string({ message: '프로젝트 제목을 입력해주세요.' })
.min(1, { message: '프로젝트 제목을 입력해주세요.' }),

maxVolunteers: z.coerce
maxVolunteers: z
.number({ message: '모집 인원을 입력해주세요.' })
.min(1, { message: '모집 인원은 1명 이상이어야 합니다.' })
.max(1000, { message: '모집 인원은 1000명 이하이어야 합니다.' }),
.min(1, { message: '모집 인원을 입력해주세요.' })
.refine(
(val) => {
const num = Number(val);
return !isNaN(num) && num >= 1 && num <= 1000;
},
{ message: '모집 인원은 1명 이상 1000명 이하여야 합니다.' }
),
Comment on lines +79 to +88
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

스키마 타입 불일치를 수정해주세요

maxVolunteersduration 필드에서 타입 불일치가 발생하고 있습니다. z.number()로 정의되어 있지만 refine 함수 내부에서는 문자열로 처리하고 있습니다.

다음과 같이 수정하여 일관성을 맞춰주세요:

-maxVolunteers: z
-  .number({ message: '모집 인원을 입력해주세요.' })
-  .min(1, { message: '모집 인원을 입력해주세요.' })
-  .refine(
-    (val) => {
-      const num = Number(val);
-      return !isNaN(num) && num >= 1 && num <= 1000;
-    },
-    { message: '모집 인원은 1명 이상 1000명 이하여야 합니다.' }
-  ),
+maxVolunteers: z
+  .string({ message: '모집 인원을 입력해주세요.' })
+  .min(1, { message: '모집 인원을 입력해주세요.' })
+  .refine(
+    (val) => {
+      const num = Number(val);
+      return !isNaN(num) && num >= 1 && num <= 1000;
+    },
+    { message: '모집 인원은 1명 이상 1000명 이하여야 합니다.' }
+  ),

-duration: z
-  .string({ message: '예상 기간을 입력해주세요.' })
-  .min(1, { message: '예상 기간을 입력해주세요.' })
-  .refine(
-    (val) => {
-      const num = Number(val);
-      return !isNaN(num) && num >= 1 && num <= 365;
-    },
-    { message: '예상 기간은 1일 이상 365일 이하여야 합니다.' }
-  ),
+duration: z
+  .string({ message: '예상 기간을 입력해주세요.' })
+  .min(1, { message: '예상 기간을 입력해주세요.' })
+  .refine(
+    (val) => {
+      const num = Number(val);
+      return !isNaN(num) && num >= 1 && num <= 365;
+    },
+    { message: '예상 기간은 1일 이상 365일 이하여야 합니다.' }
+  ),

Also applies to: 95-104

🤖 Prompt for AI Agents
In src/constants/user/projectConstants.ts around lines 79 to 88 and 95 to 104,
the maxVolunteers and duration fields are defined as z.number() but the refine
function treats the input as a string by converting it with Number(). To fix
this, remove the Number() conversion inside the refine function and directly
validate the numeric value since z.number() already ensures the input is a
number. Adjust the refine callbacks to work with numbers consistently to resolve
the type mismatch.

startDatePre: z
.string({ required_error: '종료 날짜를 입력해주세요.' })
.refine((date) => !isNaN(Date.parse(date)), {
message: '유효한 날짜를 입력해주세요.',
}),
field: z.number({ message: '진행 방식을 선택 해주세요.' }),
duration: z.coerce
.number({ message: '예상 기간을 입력해주세요.' })
.positive({ message: '예상 기간은 1 이상이어야 합니다.' })
.max(365, { message: '예상 기간은 365일을 초과할 수 없습니다.' }),
duration: z
.string({ message: '예상 기간을 입력해주세요.' })
.min(1, { message: '예상 기간을 입력해주세요.' })
.refine(
(val) => {
const num = Number(val);
return !isNaN(num) && num >= 1 && num <= 365;
},
{ message: '예상 기간은 1일 이상 365일 이하여야 합니다.' }
),
position: z
.array(z.number({ message: '숫자로 입력 되어야 합니다.' }))
.min(1, { message: '1개의 분야를 선택해주세요.' }),
Expand Down
17 changes: 17 additions & 0 deletions src/hooks/admin/useGetInquiriesPreview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useQuery } from '@tanstack/react-query';
import { Inquiries } from '../queries/keys';
import { getInquiriesPreview } from '../../api/admin/customerService/inquiry.api';

export const useGetInquiriesPreview = () => {
const {
data: allInquiriesPreviewData,
isLoading,
isFetching,
} = useQuery({
queryKey: [Inquiries.inquiriesPreview],
queryFn: () => getInquiriesPreview(),
select: (data) => data.slice(0, 5),
});

return { allInquiriesPreviewData, isLoading, isFetching };
};
Loading