diff --git a/apps/web/src/pages/ClubInformationUpdate/index.tsx b/apps/web/src/pages/ClubInformationUpdate/index.tsx
new file mode 100644
index 0000000..2372ce5
--- /dev/null
+++ b/apps/web/src/pages/ClubInformationUpdate/index.tsx
@@ -0,0 +1,250 @@
+import { useEffect, useMemo, useRef, useState, type SubmitEventHandler } from 'react';
+import { getApiErrorMessage } from '@konect/utils/api-error-message';
+import { useMutation, useQuery } from '@tanstack/react-query';
+import { useNavigate } from 'react-router-dom';
+
+import { clubInformationUpdateMutations } from '@/apis/clubInformationUpdate/mutations';
+import type { ClubCategory } from '@/apis/common/club';
+import type { University } from '@/apis/home/entity';
+import { universityClubQueries } from '@/apis/universityClub/queries';
+import { uploadImage } from '@/apis/upload';
+import {
+ ClubRequestFormLayout,
+ FieldGroup,
+ MAX_CLUB_NAME_LENGTH,
+ MAX_FULL_INTRODUCTION_LENGTH,
+ MAX_MEDIA_COUNT,
+ MAX_SHORT_DESCRIPTION_LENGTH,
+ MediaUploader,
+ RequestHeader,
+ TextInputWithCount,
+ UniversityCombobox,
+ type LocalMediaItem,
+} from '@/pages/ClubRequest/components';
+import { createLocalMediaItem, getUniversityLabel } from '@/pages/ClubRequest/utils';
+
+const ACCEPTED_IMAGE_TYPES = new Set(['image/jpeg', 'image/png', 'image/gif']);
+
+const CLUB_CATEGORY_EMOJI: Record
= {
+ ACADEMIC: 'π',
+ SPORTS: 'β½',
+ HOBBY: 'π¨',
+ RELIGION: 'π',
+ PERFORMANCE: 'π',
+ SOCIAL_SERVICE: 'π€',
+ EXHIBITION_CREATION: 'πΌοΈ',
+ ETC: 'π«',
+ JUNIOR: 'π±',
+};
+
+function ClubInformationUpdate() {
+ const navigate = useNavigate();
+ const [universityInput, setUniversityInput] = useState('');
+ const [selectedUniversity, setSelectedUniversity] = useState(null);
+ const [clubName, setClubName] = useState('');
+ const [clubQuery, setClubQuery] = useState('');
+ const [shortDescription, setShortDescription] = useState('');
+ const [fullIntroduction, setFullIntroduction] = useState('');
+ const [mediaItems, setMediaItems] = useState([]);
+ const [mediaError, setMediaError] = useState('');
+ const [formMessage, setFormMessage] = useState('');
+ const [isUploading, setIsUploading] = useState(false);
+ const mediaItemsRef = useRef([]);
+
+ const selectedUniversityId = selectedUniversity?.id;
+ const { data: clubListData, isFetching: isFetchingClubs } = useQuery({
+ ...universityClubQueries.list(selectedUniversityId ?? 0, { limit: 20, query: clubQuery }),
+ enabled: Boolean(selectedUniversityId && clubQuery),
+ });
+
+ const matchedClub = useMemo(() => {
+ const normalizedClubName = clubName.trim();
+ if (!normalizedClubName) return undefined;
+
+ return clubListData?.clubs.find((club) => club.name.trim() === normalizedClubName);
+ }, [clubListData?.clubs, clubName]);
+
+ const { mutateAsync: submitUpdateRequest, isPending } = useMutation(clubInformationUpdateMutations.submit());
+ const isSubmitting = isPending || isUploading;
+ const isFormValid = Boolean(
+ selectedUniversity && clubName.trim() && shortDescription.trim() && fullIntroduction.trim()
+ );
+
+ useEffect(() => {
+ mediaItemsRef.current = mediaItems;
+ }, [mediaItems]);
+
+ useEffect(() => {
+ return () => {
+ mediaItemsRef.current.forEach((item) => URL.revokeObjectURL(item.previewUrl));
+ };
+ }, []);
+
+ const handleUniversityInputChange = (value: string) => {
+ setUniversityInput(value);
+ setSelectedUniversity(null);
+ };
+
+ const handleUniversitySelect = (university: University, universityLabel: string) => {
+ setSelectedUniversity(university);
+ setUniversityInput(universityLabel);
+ setClubName('');
+ setClubQuery('');
+ setFormMessage('');
+ };
+
+ const handleAppendMediaItems = (files: File[]) => {
+ setMediaError('');
+
+ const validFiles = files.filter((file) => ACCEPTED_IMAGE_TYPES.has(file.type));
+ if (validFiles.length < files.length) {
+ setMediaError('JPG, PNG, GIF νμμ μ΄λ―Έμ§λ§ 첨λΆν μ μμ΅λλ€.');
+ }
+
+ setMediaItems((prevItems) => {
+ const remainingCount = MAX_MEDIA_COUNT - prevItems.length;
+ if (remainingCount <= 0) {
+ setMediaError(`μ¬μ§μ μ΅λ ${MAX_MEDIA_COUNT}κ°κΉμ§ 첨λΆν μ μμ΅λλ€.`);
+ return prevItems;
+ }
+
+ if (validFiles.length > remainingCount) {
+ setMediaError(`μ¬μ§μ μ΅λ ${MAX_MEDIA_COUNT}κ°κΉμ§ 첨λΆν μ μμ΅λλ€.`);
+ }
+
+ return [...prevItems, ...validFiles.slice(0, remainingCount).map(createLocalMediaItem)];
+ });
+ };
+
+ const handleClearMediaItems = () => {
+ setMediaItems((prevItems) => {
+ prevItems.forEach((item) => URL.revokeObjectURL(item.previewUrl));
+ return [];
+ });
+ setMediaError('');
+ };
+
+ const handleSubmit: SubmitEventHandler = async (event) => {
+ event.preventDefault();
+ setFormMessage('');
+
+ if (!selectedUniversity) {
+ setFormMessage('λνκ΅λ₯Ό μ νν΄μ£ΌμΈμ.');
+ return;
+ }
+
+ if (!matchedClub) {
+ setFormMessage(
+ isFetchingClubs
+ ? 'λμ리 μ 보λ₯Ό νμΈ μ€μ
λλ€. μ μ ν λ€μ μλν΄μ£ΌμΈμ.'
+ : 'μ νν νκ΅μ λ±λ‘λ λμ리λͺ
μ μ νν μ
λ ₯ν΄μ£ΌμΈμ.'
+ );
+ return;
+ }
+
+ try {
+ setIsUploading(true);
+ const uploadedImages = await Promise.all(mediaItems.map((item) => uploadImage(item.file, 'CLUB')));
+ setIsUploading(false);
+
+ await submitUpdateRequest({
+ clubId: matchedClub.id,
+ body: {
+ universityName: getUniversityLabel(selectedUniversity),
+ clubName: clubName.trim(),
+ clubCategory: matchedClub.category,
+ clubTopic: matchedClub.topic,
+ clubEmoji: CLUB_CATEGORY_EMOJI[matchedClub.category],
+ shortDescription: shortDescription.trim(),
+ fullIntroduction: fullIntroduction.trim(),
+ imageUrls: uploadedImages.map(({ fileUrl }) => fileUrl),
+ },
+ });
+
+ handleClearMediaItems();
+ setClubName('');
+ setClubQuery('');
+ setShortDescription('');
+ setFullIntroduction('');
+ navigate('/clubs/register/complete');
+ } catch (error) {
+ setIsUploading(false);
+ setFormMessage(getApiErrorMessage(error, 'λμ리 μ 보 μμ μμ²μ μ€ν¨νμ΅λλ€.'));
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {
+ const value = event.target.value;
+ setClubName(value);
+ setClubQuery(value.trim());
+ setFormMessage('');
+ }}
+ placeholder="(νμ) λμ리λͺ
μ μ
λ ₯ν΄μ£ΌμΈμ."
+ maxLength={MAX_CLUB_NAME_LENGTH}
+ ariaLabel="λμ리λͺ
"
+ />
+
+
+
+ setShortDescription(event.target.value)}
+ placeholder="(νμ) λμ리μ ν μ€ μκ°λ₯Ό μ
λ ₯ν΄μ£ΌμΈμ."
+ maxLength={MAX_SHORT_DESCRIPTION_LENGTH}
+ ariaLabel="ν μ€ μκ°"
+ />
+
+
+
+
+
+ setFullIntroduction(event.target.value)}
+ placeholder="(νμ) λμ리λ₯Ό μκ°ν μ μλ λ΄μ©μ μμ λ‘κ² μμ±ν΄μ£ΌμΈμ."
+ maxLength={MAX_FULL_INTRODUCTION_LENGTH}
+ ariaLabel="λμ리 μκ°"
+ />
+
+
+
+
+ );
+}
+
+export default ClubInformationUpdate;
diff --git a/apps/web/src/pages/ClubRegistration/index.tsx b/apps/web/src/pages/ClubRegistration/index.tsx
new file mode 100644
index 0000000..a353f5b
--- /dev/null
+++ b/apps/web/src/pages/ClubRegistration/index.tsx
@@ -0,0 +1,272 @@
+import { useEffect, useRef, useState, type ChangeEvent, type SubmitEventHandler } from 'react';
+import { getApiErrorMessage } from '@konect/utils/api-error-message';
+import { useMutation } from '@tanstack/react-query';
+import { useNavigate } from 'react-router-dom';
+
+import { clubRegistrationMutations } from '@/apis/clubRegistration/mutations';
+import { CLUB_CATEGORY, type ClubCategory } from '@/apis/common/club';
+import type { University } from '@/apis/home/entity';
+import { uploadImage } from '@/apis/upload';
+import {
+ ClubRequestFormLayout,
+ FieldGroup,
+ MAX_CLUB_NAME_LENGTH,
+ MAX_FULL_INTRODUCTION_LENGTH,
+ MAX_MEDIA_COUNT,
+ MAX_SHORT_DESCRIPTION_LENGTH,
+ MediaUploader,
+ PlainTextInput,
+ RequestHeader,
+ TextInputWithCount,
+ UniversityCombobox,
+ type LocalMediaItem,
+} from '@/pages/ClubRequest/components';
+import { createLocalMediaItem, getUniversityLabel } from '@/pages/ClubRequest/utils';
+
+const ACCEPTED_IMAGE_TYPES = new Set(['image/jpeg', 'image/png', 'image/gif']);
+
+const CLUB_CATEGORY_OPTIONS: { label: string; value: ClubCategory }[] = [
+ { label: 'νμ ', value: CLUB_CATEGORY.ACADEMIC },
+ { label: 'μ΄λ', value: CLUB_CATEGORY.SPORTS },
+ { label: 'μ·¨λ―Έ', value: CLUB_CATEGORY.HOBBY },
+ { label: 'μ’
κ΅', value: CLUB_CATEGORY.RELIGION },
+ { label: '곡μ°', value: CLUB_CATEGORY.PERFORMANCE },
+ { label: 'λ΄μ¬', value: CLUB_CATEGORY.SOCIAL_SERVICE },
+ { label: 'μ μ/μ°½μ', value: CLUB_CATEGORY.EXHIBITION_CREATION },
+ { label: 'κΈ°ν', value: CLUB_CATEGORY.ETC },
+ { label: 'μ£Όλμ΄', value: CLUB_CATEGORY.JUNIOR },
+];
+
+function ClubRegistration() {
+ const navigate = useNavigate();
+ const [universityInput, setUniversityInput] = useState('');
+ const [selectedUniversity, setSelectedUniversity] = useState(null);
+ const [clubName, setClubName] = useState('');
+ const [clubEmoji, setClubEmoji] = useState('π');
+ const [clubCategory, setClubCategory] = useState('');
+ const [clubTopic, setClubTopic] = useState('');
+ const [shortDescription, setShortDescription] = useState('');
+ const [fullIntroduction, setFullIntroduction] = useState('');
+ const [mediaItems, setMediaItems] = useState([]);
+ const [mediaError, setMediaError] = useState('');
+ const [formMessage, setFormMessage] = useState('');
+ const [isUploading, setIsUploading] = useState(false);
+ const mediaItemsRef = useRef([]);
+
+ const { mutateAsync: submitRegistrationRequest, isPending } = useMutation(clubRegistrationMutations.submit());
+ const isSubmitting = isPending || isUploading;
+ const isFormValid = Boolean(
+ selectedUniversity &&
+ clubName.trim() &&
+ clubEmoji.trim() &&
+ clubCategory &&
+ clubTopic.trim() &&
+ shortDescription.trim() &&
+ fullIntroduction.trim()
+ );
+
+ useEffect(() => {
+ mediaItemsRef.current = mediaItems;
+ }, [mediaItems]);
+
+ useEffect(() => {
+ return () => {
+ mediaItemsRef.current.forEach((item) => URL.revokeObjectURL(item.previewUrl));
+ };
+ }, []);
+
+ const handleAppendMediaItems = (files: File[]) => {
+ setMediaError('');
+
+ const validFiles = files.filter((file) => ACCEPTED_IMAGE_TYPES.has(file.type));
+ if (validFiles.length < files.length) {
+ setMediaError('JPG, PNG, GIF νμμ μ΄λ―Έμ§λ§ 첨λΆν μ μμ΅λλ€.');
+ }
+
+ setMediaItems((prevItems) => {
+ const remainingCount = MAX_MEDIA_COUNT - prevItems.length;
+ if (remainingCount <= 0) {
+ setMediaError(`μ¬μ§μ μ΅λ ${MAX_MEDIA_COUNT}κ°κΉμ§ 첨λΆν μ μμ΅λλ€.`);
+ return prevItems;
+ }
+
+ if (validFiles.length > remainingCount) {
+ setMediaError(`μ¬μ§μ μ΅λ ${MAX_MEDIA_COUNT}κ°κΉμ§ 첨λΆν μ μμ΅λλ€.`);
+ }
+
+ return [...prevItems, ...validFiles.slice(0, remainingCount).map(createLocalMediaItem)];
+ });
+ };
+
+ const handleClearMediaItems = () => {
+ setMediaItems((prevItems) => {
+ prevItems.forEach((item) => URL.revokeObjectURL(item.previewUrl));
+ return [];
+ });
+ setMediaError('');
+ };
+
+ const handleCategoryChange = (event: ChangeEvent) => {
+ setClubCategory(event.target.value as ClubCategory | '');
+ };
+
+ const handleSubmit: SubmitEventHandler = async (event) => {
+ event.preventDefault();
+ setFormMessage('');
+
+ if (!selectedUniversity || !clubCategory) {
+ setFormMessage('νμ μ 보λ₯Ό λͺ¨λ μ
λ ₯ν΄μ£ΌμΈμ.');
+ return;
+ }
+
+ try {
+ setIsUploading(true);
+ const uploadedImages = await Promise.all(mediaItems.map((item) => uploadImage(item.file, 'CLUB')));
+ setIsUploading(false);
+
+ await submitRegistrationRequest({
+ body: {
+ universityName: getUniversityLabel(selectedUniversity),
+ clubName: clubName.trim(),
+ clubCategory,
+ clubTopic: clubTopic.trim(),
+ clubEmoji: clubEmoji.trim(),
+ shortDescription: shortDescription.trim(),
+ fullIntroduction: fullIntroduction.trim(),
+ imageUrls: uploadedImages.map(({ fileUrl }) => fileUrl),
+ },
+ });
+
+ handleClearMediaItems();
+ setClubName('');
+ setClubEmoji('π');
+ setClubCategory('');
+ setClubTopic('');
+ setShortDescription('');
+ setFullIntroduction('');
+ navigate('/clubs/register/complete');
+ } catch (error) {
+ setIsUploading(false);
+ setFormMessage(getApiErrorMessage(error, 'μ κ· λμ리 λ±λ‘ μμ²μ μ€ν¨νμ΅λλ€.'));
+ }
+ };
+
+ return (
+
+
+
+
+
+
+ {
+ setUniversityInput(value);
+ setSelectedUniversity(null);
+ }}
+ onSelect={(university, universityLabel) => {
+ setSelectedUniversity(university);
+ setUniversityInput(universityLabel);
+ setFormMessage('');
+ }}
+ />
+
+
+
+ setClubName(event.target.value)}
+ placeholder="(νμ) λμ리λͺ
μ μ
λ ₯ν΄μ£ΌμΈμ."
+ maxLength={MAX_CLUB_NAME_LENGTH}
+ ariaLabel="λμ리λͺ
"
+ />
+
+
+
+
+
+ setClubEmoji(event.target.value)}
+ maxLength={8}
+ aria-label="λμ리 μ΄λͺ¨μ§"
+ />
+
+
+
+
+
+
+
+
+
+
+ setClubTopic(event.target.value)}
+ placeholder="(νμ) ex) λꡬ, λ°΄λ, μ¬μ§, λμ€"
+ ariaLabel="λμ리 μ£Όμ "
+ maxLength={20}
+ />
+
+
+
+
+ setShortDescription(event.target.value)}
+ placeholder="(νμ) λμ리μ ν μ€ μκ°λ₯Ό μ
λ ₯ν΄μ£ΌμΈμ."
+ maxLength={MAX_SHORT_DESCRIPTION_LENGTH}
+ ariaLabel="ν μ€ μκ°"
+ />
+
+
+
+
+
+ setFullIntroduction(event.target.value)}
+ placeholder="(νμ) λμ리λ₯Ό μκ°ν μ μλ λ΄μ©μ μμ λ‘κ² μμ±ν΄μ£ΌμΈμ."
+ maxLength={MAX_FULL_INTRODUCTION_LENGTH}
+ ariaLabel="λμ리 μκ°"
+ />
+
+
+
+
+ );
+}
+
+export default ClubRegistration;
diff --git a/apps/web/src/pages/ClubRequest/components.tsx b/apps/web/src/pages/ClubRequest/components.tsx
new file mode 100644
index 0000000..529e580
--- /dev/null
+++ b/apps/web/src/pages/ClubRequest/components.tsx
@@ -0,0 +1,360 @@
+import { useId, useState, type ChangeEvent, type DragEvent, type ReactNode, type SubmitEventHandler } from 'react';
+import { cn } from '@konect/utils/cn';
+import { useDebouncedCallback } from '@konect/utils/use-debounced-callback';
+import { useSuspenseQuery } from '@tanstack/react-query';
+
+import type { University } from '@/apis/home/entity';
+import { homeQueries } from '@/apis/home/queries';
+import ArrowDropdownIcon from '@/assets/svg/arrow_drop_down-icon.svg';
+
+import { getUniversityLabel } from './utils';
+
+export const MAX_CLUB_NAME_LENGTH = 20;
+export const MAX_SHORT_DESCRIPTION_LENGTH = 100;
+export const MAX_FULL_INTRODUCTION_LENGTH = 100;
+export const MAX_MEDIA_COUNT = 5;
+
+export interface LocalMediaItem {
+ file: File;
+ id: string;
+ previewUrl: string;
+}
+
+interface UniversityComboboxProps {
+ inputValue: string;
+ onInputChange: (value: string) => void;
+ onSelect: (university: University, universityLabel: string) => void;
+ selectedUniversity: University | null;
+}
+
+export function RequestHeader({ description, title }: { description: string; title: string }) {
+ return (
+
+
+
+ {title}
+
+ {description}
+
+ );
+}
+
+export function ClubRequestFormLayout({
+ children,
+ formMessage,
+ isSubmitDisabled,
+ isSubmitting,
+ onSubmit,
+}: {
+ children: ReactNode;
+ formMessage: string;
+ isSubmitDisabled: boolean;
+ isSubmitting: boolean;
+ onSubmit: SubmitEventHandler;
+}) {
+ return (
+
+ );
+}
+
+export function FieldGroup({
+ children,
+ helperText,
+ label,
+ required = false,
+ trailingText,
+}: {
+ children: ReactNode;
+ helperText?: string;
+ label: string;
+ required?: boolean;
+ trailingText?: string;
+}) {
+ return (
+
+
+
+ {trailingText && (
+ {trailingText}
+ )}
+
+ {children}
+ {helperText &&
{helperText}
}
+
+ );
+}
+
+export function TextInputWithCount({
+ ariaLabel,
+ maxLength,
+ onChange,
+ placeholder,
+ value,
+}: {
+ ariaLabel: string;
+ maxLength: number;
+ onChange: (event: ChangeEvent) => void;
+ placeholder: string;
+ value: string;
+}) {
+ return (
+
+
+
+ {value.length}/{maxLength}
+
+
+ );
+}
+
+export function PlainTextInput({
+ ariaLabel,
+ className,
+ maxLength,
+ onChange,
+ placeholder,
+ value,
+}: {
+ ariaLabel: string;
+ className?: string;
+ maxLength?: number;
+ onChange: (event: ChangeEvent) => void;
+ placeholder: string;
+ value: string;
+}) {
+ return (
+
+
+
+ );
+}
+
+export function UniversityCombobox({
+ inputValue,
+ onInputChange,
+ onSelect,
+ selectedUniversity,
+}: UniversityComboboxProps) {
+ const [query, setQuery] = useState('');
+ const [isOpen, setIsOpen] = useState(false);
+ const listboxId = useId();
+ const updateQuery = useDebouncedCallback((value: string) => {
+ setQuery(value.trim());
+ });
+
+ const { data: homeData } = useSuspenseQuery(homeQueries.detail(query ? { query } : {}));
+ const universityOptions = homeData.universities ?? [];
+
+ const handleInputChange = (event: ChangeEvent) => {
+ const value = event.target.value;
+ onInputChange(value);
+ setIsOpen(true);
+ updateQuery(value);
+ };
+
+ const handleUniversitySelect = (university: University) => {
+ const universityLabel = getUniversityLabel(university);
+ onSelect(university, universityLabel);
+ setQuery(universityLabel);
+ setIsOpen(false);
+ };
+
+ return (
+ {
+ const nextTarget = event.relatedTarget;
+ if (!(nextTarget instanceof Node) || !event.currentTarget.contains(nextTarget)) {
+ setIsOpen(false);
+ }
+ }}
+ >
+
+
setIsOpen(true)}
+ placeholder="(νμ) λνκ΅λ₯Ό μ ννμΈμ."
+ aria-autocomplete="list"
+ aria-controls={isOpen ? listboxId : undefined}
+ aria-expanded={isOpen}
+ aria-label="λνκ΅λͺ
"
+ autoComplete="off"
+ role="combobox"
+ />
+
+
+
+ {isOpen && (
+
+ {universityOptions.length > 0 ? (
+
+ {universityOptions.map((university) => (
+ -
+
+
+ ))}
+
+ ) : (
+
+ κ²μ κ²°κ³Όκ° μμ΅λλ€.
+
+ )}
+
+ )}
+
+ );
+}
+
+export function MediaUploader({
+ mediaError,
+ mediaItems,
+ onAppendMediaFiles,
+ onClearMediaItems,
+}: {
+ mediaError: string;
+ mediaItems: LocalMediaItem[];
+ onAppendMediaFiles: (files: File[]) => void;
+ onClearMediaItems: () => void;
+}) {
+ const handleMediaInputChange = (event: ChangeEvent) => {
+ onAppendMediaFiles(Array.from(event.currentTarget.files ?? []));
+ event.currentTarget.value = '';
+ };
+
+ const handleMediaDrop = (event: DragEvent) => {
+ event.preventDefault();
+ onAppendMediaFiles(Array.from(event.dataTransfer.files));
+ };
+
+ return (
+
+
+ {mediaItems.length > 0 && (
+
+ )}
+
+ λμ리λ₯Ό μκ°ν μ μλ μ¬μ§μ΄λ μμμ 첨λΆν΄μ£ΌμΈμ.
+
+ 첨λΆν μ¬μ§μ μμΈ νμ΄μ§μμ 16:9 μμ μμ νμλλ©°, μ΄λ―Έμ§ λΉμ¨μ λ°λΌ μ’μ° λλ μν μ¬λ°±μ΄ μκΈΈ μ
+ μμ΅λλ€.
+
+ {mediaError && {mediaError}
}
+
+ );
+}
+
+export function RequestNotice() {
+ return (
+
+
μ
λ ₯ν΄μ£Όμ μ 보λ λ΄λΆ νμΈ ν λμ리 μμΈ νμ΄μ§μ λ°μλ©λλ€.
+
νμμ 보 νΉμ λΆμ μ ν λ΄μ©μ λ°μμ΄ μ νλ μ μμ΅λλ€.
+
+ );
+}
diff --git a/apps/web/src/pages/ClubRequest/utils.ts b/apps/web/src/pages/ClubRequest/utils.ts
new file mode 100644
index 0000000..e83eaed
--- /dev/null
+++ b/apps/web/src/pages/ClubRequest/utils.ts
@@ -0,0 +1,15 @@
+import type { University } from '@/apis/home/entity';
+
+import type { LocalMediaItem } from './components';
+
+export function createLocalMediaItem(file: File): LocalMediaItem {
+ return {
+ file,
+ id: `${file.name}-${file.lastModified}-${Date.now()}-${Math.random()}`,
+ previewUrl: URL.createObjectURL(file),
+ };
+}
+
+export function getUniversityLabel(university: University) {
+ return university.campusName ? `${university.name} ${university.campusName}` : university.name;
+}
diff --git a/apps/web/src/pages/ClubRequestComplete/index.tsx b/apps/web/src/pages/ClubRequestComplete/index.tsx
new file mode 100644
index 0000000..6380e37
--- /dev/null
+++ b/apps/web/src/pages/ClubRequestComplete/index.tsx
@@ -0,0 +1,36 @@
+import { Link } from 'react-router-dom';
+
+import CompleteImage from '@/assets/image/complete-cat.jpg';
+
+function ClubRequestComplete() {
+ return (
+
+
+
+
+
+
λμ리 μκ° μ μ‘μ΄ μλ£λμμ΄μ!
+
+ 보λ΄μ£Όμ λμ리 μκ° λ΄μ©μ νμΈ ν λμ리 μμΈ νμ΄μ§μ λ°μλ©λλ€.
+
+ κ²ν κ³Όμ μ λ°λΌ λ°μκΉμ§ μκ°μ΄ μμλ©λλ€.
+
+
+
+
+ λ©μΈμΌλ‘
+
+
+
+ );
+}
+
+export default ClubRequestComplete;
diff --git a/apps/web/src/pages/RegisterClub/index.tsx b/apps/web/src/pages/RegisterClub/index.tsx
index 48b5051..0cd53b6 100644
--- a/apps/web/src/pages/RegisterClub/index.tsx
+++ b/apps/web/src/pages/RegisterClub/index.tsx
@@ -1,6 +1,10 @@
+import type { ReactNode } from 'react';
+import { Link } from 'react-router-dom';
+
import EditClub from '@/assets/edit-club-detail.png';
import NewClub from '@/assets/new-club.png';
import Register from '@/assets/register-club.png';
+
export default function RegisterClub() {
const registerClubCards = [
{
@@ -9,6 +13,7 @@ export default function RegisterClub() {
title: 'λμ리 μ 보 μμ ',
description: 'μ΄λ―Έ KONECTμ λ±λ‘λ λμ리μ μκ°, μ¬μ§, μμΈμ 보λ₯Ό μΆκ°νκ±°λ μμ ν μ μμ΄μ',
target: 'λμ : λμ리 νμ₯, μμμ§',
+ to: '/clubs/information-update-requests',
},
{
image: NewClub,
@@ -16,6 +21,7 @@ export default function RegisterClub() {
title: 'μ κ· λμ리 λ±λ‘',
description: 'μμ§ KONECTμ λ±λ‘λμ§ μμ λμ리μ κΈ°λ³Έ μ 보μ μκ° μ 보λ₯Ό μ μΆν μ μμ΄μ.',
target: 'λμ : λ―Έλ±λ‘λ λμ리μ κ΄κ³μ',
+ to: '/clubs/registration-requests',
},
{
image: Register,
@@ -38,19 +44,44 @@ export default function RegisterClub() {
{registerClubCards.map((card) => (
-
+
{card.title}
{card.description}
{card.target}
-
+
))}
);
}
+
+function RegisterClubCard({
+ card,
+ children,
+}: {
+ card: {
+ description: string;
+ image: string;
+ imageAlt: string;
+ target: string;
+ title: string;
+ to?: string;
+ };
+ children: ReactNode;
+}) {
+ const className =
+ 'border-text-100 focus-visible:outline-primary-500 flex h-92.75 w-82.75 flex-col items-center gap-10 rounded-[20px] border bg-[#ffffff] px-7.5 py-10 transition-[border-color,box-shadow] hover:border-primary-500 hover:shadow-[0_0_30px_0_rgba(105,191,223,0.30)] focus-visible:outline-2 focus-visible:outline-offset-2';
+
+ if (card.to) {
+ return (
+
+ {children}
+
+ );
+ }
+
+ return {children}
;
+}
diff --git a/apps/web/src/pages/UniversityClubList/index.tsx b/apps/web/src/pages/UniversityClubList/index.tsx
index 067dadf..900d6b9 100644
--- a/apps/web/src/pages/UniversityClubList/index.tsx
+++ b/apps/web/src/pages/UniversityClubList/index.tsx
@@ -175,7 +175,7 @@ function ClubCard({ club }: { club: UniversityClub }) {
type="button"
to={`/clubs/${club.id}`}
>
-
+
{club.name}