diff --git a/apps/frontend/app/(main)/settings/page.tsx b/apps/frontend/app/(main)/settings/page.tsx
new file mode 100644
index 0000000000..fa7821ab7b
--- /dev/null
+++ b/apps/frontend/app/(main)/settings/page.tsx
@@ -0,0 +1,559 @@
+'use client'
+
+import { Button } from '@/components/ui/button'
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList
+} from '@/components/ui/command'
+import { Input } from '@/components/ui/input'
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger
+} from '@/components/ui/popover'
+import { ScrollArea } from '@/components/ui/scroll-area'
+import { majors } from '@/lib/constants'
+import { cn, safeFetcher, safeFetcherWithAuth } from '@/lib/utils'
+import invisible from '@/public/24_invisible.svg'
+import visible from '@/public/24_visible.svg'
+import codedangSymbol from '@/public/codedang-editor.svg'
+import { zodResolver } from '@hookform/resolvers/zod'
+import type { NavigateOptions } from 'next/dist/shared/lib/app-router-context.shared-runtime'
+import Image from 'next/image'
+import { useRouter } from 'next/navigation'
+import React, { useEffect } from 'react'
+import { useState } from 'react'
+import { useForm } from 'react-hook-form'
+import { FaCheck, FaChevronDown } from 'react-icons/fa6'
+import { toast } from 'sonner'
+import { z } from 'zod'
+
+interface SettingsFormat {
+ currentPassword: string
+ newPassword: string
+ confirmPassword: string
+ realName: string
+ studentId: string
+}
+
+interface getProfile {
+ username: string // ID
+ userProfile: {
+ realName: string
+ }
+ studentId: string
+ major: string
+}
+
+type UpdatePayload = Partial<{
+ password: string
+ newPassword: string
+ realName: string
+ studentId: string
+ major: string
+}>
+
+// const schemaUpdate = z.object({
+// studentId: z.string().optional(),
+// password: z.string().optional(),
+// newPassword: z.string().optional(),
+// realName: z.string().optional(),
+// major: z.string().optional()
+// })
+
+const schemaSettings = z.object({
+ currentPassword: z.string().min(1, { message: 'Required' }).optional(),
+ newPassword: z
+ .string()
+ .min(1)
+ .min(8)
+ .max(20)
+ .refine((data) => {
+ const invalidPassword = /^([a-z]*|[A-Z]*|[0-9]*|[^a-zA-Z0-9]*)$/
+ return !invalidPassword.test(data)
+ })
+ .optional(),
+ confirmPassword: z.string().optional(),
+ realName: z
+ .string()
+ .regex(/^[a-zA-Z\s]+$/, { message: 'Only English Allowed' })
+ .optional(),
+ studentId: z.string().optional()
+})
+
+function requiredMessage(message?: string) {
+ return (
+
+ {message}
+
+ )
+}
+
+export default function Page() {
+ const [defaultProfileValues, setdefaultProfileValues] = useState({
+ username: '',
+ userProfile: {
+ realName: ''
+ },
+ studentId: '',
+ major: ''
+ })
+
+ useEffect(() => {
+ const fetchDefaultProfile = async () => {
+ try {
+ const data: getProfile = await safeFetcherWithAuth.get('user').json()
+ setMajorValue(data.major)
+ setdefaultProfileValues(data)
+ setIsLoading(false)
+ } catch (error) {
+ console.error('Failed to fetch profile:', error)
+ toast.error('Failed to load profile data')
+ setIsLoading(false)
+ }
+ }
+ fetchDefaultProfile()
+ }, [])
+
+ const {
+ register,
+ handleSubmit,
+ getValues,
+ setValue,
+ watch,
+ formState: { errors, isDirty }
+ } = useForm({
+ resolver: zodResolver(schemaSettings),
+ mode: 'onChange',
+ defaultValues: {
+ currentPassword: '',
+ newPassword: '',
+ confirmPassword: '',
+ realName: defaultProfileValues.userProfile.realName,
+ studentId: defaultProfileValues.studentId
+ }
+ })
+
+ // const beforeUnloadHandler = (event: BeforeUnloadEvent) => {
+ // // Recommended
+ // event.preventDefault()
+
+ // // Included for legacy support, e.g. Chrome/Edge < 119
+ // event.returnValue = true
+ // return true
+ // }
+
+ /**
+ * Prompt the user with a confirmation dialog when they try to navigate away from the page.
+ */
+ const useConfirmNavigation = () => {
+ const router = useRouter()
+ useEffect(() => {
+ const originalPush = router.push
+ const newPush = (
+ href: string,
+ options?: NavigateOptions | undefined
+ ): void => {
+ const isConfirmed = window.confirm(
+ 'Are you sure you want to leave?\nYour changes have not been saved.\nIf you leave this page, all changes will be lost.\nDo you still want to proceed?'
+ )
+ // Error occurs if I just put 'href' without 'href === ...' code..
+ if (isConfirmed) {
+ if (
+ href === '/settings' ||
+ href === '/notice' ||
+ href === '/problem' ||
+ href === '/contest' ||
+ href === '/'
+ ) {
+ originalPush(href, options)
+ }
+ }
+ }
+ router.push = newPush
+ // window.onbeforeunload = beforeUnloadHandler
+ return () => {
+ router.push = originalPush
+ // window.onbeforeunload = null
+ }
+ }, [router, isDirty])
+ }
+
+ useConfirmNavigation()
+
+ const [isCheckButtonClicked, setIsCheckButtonClicked] =
+ useState(false)
+ const [isPasswordCorrect, setIsPasswordCorrect] = useState(false)
+ const [newPasswordAble, setNewPasswordAble] = useState(false)
+ const [passwordShow, setPasswordShow] = useState(false)
+ const [newPasswordShow, setNewPasswordShow] = useState(false)
+ const [confirmPasswordShow, setConfirmPasswordShow] = useState(false)
+ const [majorOpen, setMajorOpen] = useState(false)
+ const [majorValue, setMajorValue] = useState('')
+ const currentPassword = watch('currentPassword')
+ const newPassword = watch('newPassword')
+ const confirmPassword = watch('confirmPassword')
+ const realName = watch('realName')
+ const isPasswordsMatch = newPassword === confirmPassword && newPassword !== ''
+ const [isLoading, setIsLoading] = useState(true)
+ // saveAble1, saveAble2 둘 중 하나라도 true 면 Save 버튼 활성화
+ const saveAblePassword: boolean =
+ !!currentPassword &&
+ !!newPassword &&
+ !!confirmPassword &&
+ isPasswordCorrect &&
+ newPasswordAble &&
+ isPasswordsMatch
+ const saveAbleOthers: boolean =
+ !!realName || !!(majorValue !== defaultProfileValues.major)
+ const saveAble = saveAblePassword || saveAbleOthers
+
+ // New Password Input 창과 Re-enter Password Input 창의 border 색상을, 일치하는지 여부에 따라 바꿈
+ useEffect(() => {
+ if (isPasswordsMatch) {
+ setValue('newPassword', newPassword)
+ setValue('confirmPassword', confirmPassword)
+ }
+ }, [isPasswordsMatch, newPassword, confirmPassword])
+
+ const onSubmit = async (data: SettingsFormat) => {
+ try {
+ // 필요 없는 필드 제외 (defaultProfileValues와 값이 같은 것들은 제외)
+ const updatePayload: UpdatePayload = {}
+ if (data.realName !== defaultProfileValues.userProfile.realName) {
+ updatePayload.realName = data.realName
+ }
+ if (majorValue !== defaultProfileValues.major) {
+ updatePayload.major = majorValue
+ }
+ if (data.currentPassword !== 'tmppassword1') {
+ updatePayload.password = data.currentPassword
+ }
+ if (data.newPassword !== 'tmppassword1') {
+ updatePayload.newPassword = data.newPassword
+ }
+
+ const response = await safeFetcherWithAuth.patch('user', {
+ json: updatePayload
+ })
+ if (response.ok) {
+ toast.success('Successfully updated your information')
+ setTimeout(() => {
+ window.location.reload()
+ }, 1500)
+ }
+ } catch (error) {
+ console.error(error)
+ toast.error('Failed to update your information, Please try again')
+ setTimeout(() => {
+ window.location.reload()
+ }, 1500)
+ }
+ }
+
+ const onSubmitClick = () => {
+ return () => {
+ // submit 되기위해, watch로 확인되는 값이 default값과 같으면 setValue를 통해서 defaultProfileValues로 변경
+ if (realName === '') {
+ setValue('realName', defaultProfileValues.userProfile.realName)
+ }
+ if (majorValue === defaultProfileValues.major) {
+ setMajorValue(defaultProfileValues.major)
+ }
+ if (currentPassword === '') {
+ setValue('currentPassword', 'tmppassword1')
+ }
+ if (newPassword === '') {
+ setValue('newPassword', 'tmppassword1')
+ }
+ if (confirmPassword === '') {
+ setValue('confirmPassword', 'tmppassword1')
+ }
+ }
+ }
+
+ const checkPassword = async () => {
+ setIsCheckButtonClicked(true)
+ try {
+ const response = await safeFetcher.post('auth/login', {
+ json: {
+ username: defaultProfileValues.username,
+ password: currentPassword
+ }
+ })
+
+ if (response.status === 201) {
+ setIsPasswordCorrect(true)
+ setNewPasswordAble(true)
+ }
+ } catch {
+ console.error('Failed to check password')
+ }
+ }
+
+ return (
+
+
+
+
Online Judge Platform for SKKU
+
+
+
+
+ )
+}
diff --git a/apps/frontend/components/auth/HeaderAuthPanel.tsx b/apps/frontend/components/auth/HeaderAuthPanel.tsx
index ba12eb0395..3efe1bc4a7 100644
--- a/apps/frontend/components/auth/HeaderAuthPanel.tsx
+++ b/apps/frontend/components/auth/HeaderAuthPanel.tsx
@@ -82,6 +82,18 @@ export default function HeaderAuthPanel({
)}
+
+
+ Settings
+
+