Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
498ad1d
feat(fe): redesign settings page for whitelist removal
ojongii Jun 28, 2026
c86b4df
fix(fe): guard settings page and refine email validation
ojongii Jul 2, 2026
b3ee63d
fix(fe): allow svg images from dicebear and show update modal only fo…
ojongii Jul 5, 2026
3e62851
fix(fe): remove unnecessary ts-expect-error directive
ojongii Jul 5, 2026
c31cb4e
refactor(fe): extract inline SVG icons to separate files in settings …
ojongii Jul 6, 2026
d98bc82
fix(fe): cleanup code
ojongii Jul 6, 2026
b88a420
fix(fe): apply feedback
ojongii Jul 6, 2026
82c04cf
fix(fe): resolve conflict
ojongii Jul 6, 2026
b63e766
Merge branch 't2691-impl-verification-logic-for-whitelist-removal' in…
ojongii Jul 6, 2026
b6dc905
chore(fe): cleanup code
ojongii Jul 6, 2026
8dee318
Merge branch 't2731-make-settings-page-for-whitelist-removal' of http…
ojongii Jul 6, 2026
6eaf0c5
feat(fe): implement account deletion feature
ojongii Jul 6, 2026
9209626
feat(fe): implement account linking and refactor settings page
ojongii Jul 13, 2026
e57a42a
fix(fe): fix toast not showing on oauth redirect by wrapping with Sus…
ojongii Jul 13, 2026
e9427b3
feat(fe): improve settings page UX and align with figma design
ojongii Jul 14, 2026
ecd3484
feat(fe): improve settings page UX and fix edge cases for whitelist r…
ojongii Jul 16, 2026
5a9e4c9
fix(fe): sync university list with signup page
ojongii Jul 16, 2026
dec3a9f
refactor(fe): replace CollegeSection and MajorSection with shared sea…
ojongii Jul 16, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
'use client'

import { safeFetcherWithAuth } from '@/libs/utils'
import githubIcon from '@/public/icons/github.svg'
import googleIcon from '@/public/icons/google.svg'
import kakaoIcon from '@/public/icons/kakao.svg'
import naverIcon from '@/public/icons/naver.svg'
import Image from 'next/image'
import { useRouter, useSearchParams } from 'next/navigation'
import { useEffect, useRef, useState } from 'react'
import { toast } from 'sonner'
import { useSettingsContext } from './context'

const PROVIDERS = [
{
id: 'google',
name: 'Google',
icon: googleIcon,
iconSize: 32,
bgColor: 'bg-white border border-line'
},
{
id: 'kakao',
name: '카카오톡',
icon: kakaoIcon,
iconSize: 28,
bgColor: 'bg-[#fbe300]'
},
{
id: 'github',
name: 'GitHub',
icon: githubIcon,
iconSize: 28,
bgColor: 'bg-black'
},
{
id: 'naver',
name: 'Naver',
icon: naverIcon,
iconSize: 28,
bgColor: 'bg-[#03cf5d]'
}
]

export function AccountLinkingSection() {
const { defaultProfileValues } = useSettingsContext()
const router = useRouter()
const searchParams = useSearchParams()
const [loadingProvider, setLoadingProvider] = useState<string | null>(null)
const [locallyLinked, setLocallyLinked] = useState<Set<string>>(new Set())
const [locallyUnlinked, setLocallyUnlinked] = useState<Set<string>>(new Set())
const hasProcessed = useRef(false)

const connectedProviders = new Set(
[...(defaultProfileValues.linkedProviders ?? []), ...locallyLinked].filter(
(p) => !locallyUnlinked.has(p)
)
)

const oauthToken = searchParams.get('oauthToken')
const error = searchParams.get('error')

useEffect(() => {
if (hasProcessed.current) {
return
}
if (oauthToken) {
hasProcessed.current = true
const link = async () => {
try {
await safeFetcherWithAuth.post('auth/social-link', {
json: { oauthToken }
})
toast.success('카카오 계정이 연결되었습니다')
setLocallyLinked((prev) => new Set([...prev, 'kakao']))
} catch {
toast.error('계정 연결에 실패했습니다. 다시 시도해주세요')
} finally {
router.replace('/settings')
}
}
link()
} else if (error === 'already-linked') {
hasProcessed.current = true
toast.error('이미 다른 계정에 연결된 카카오 계정입니다')
router.replace('/settings')
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

const handleConnect = (providerId: string) => {
if (providerId === 'kakao') {
window.location.href = `${process.env.NEXT_PUBLIC_BASEURL}/auth/kakao/link`
return
}
toast.info(`${providerId} 연결 기능은 준비 중입니다.`)
}

const handleDisconnect = async (providerId: string) => {
if (providerId !== 'kakao') {
toast.info(`${providerId} 연결 해제 기능은 준비 중입니다.`)
return
}
setLoadingProvider(providerId)
try {
await safeFetcherWithAuth.delete(`auth/social-link/${providerId}`)
toast.success('카카오 계정 연결이 해제되었습니다')
setLocallyUnlinked((prev) => new Set([...prev, providerId]))
setLocallyLinked((prev) => {
const next = new Set(prev)
next.delete(providerId)
return next
})
} catch {
toast.error('연결 해제에 실패했습니다. 다시 시도해주세요')
} finally {
setLoadingProvider(null)
}
}

return (
<div className="border-color-cool-neutral-90 flex flex-col gap-7 rounded-2xl border bg-white px-6 py-7">
{PROVIDERS.map((provider) => {
const isConnected = connectedProviders.has(provider.id)
return (
<div key={provider.id} className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="relative size-12">
<div className={`size-full rounded-xl ${provider.bgColor}`} />
<div className="absolute inset-0 flex items-center justify-center">
<Image
src={provider.icon}
alt={provider.name}
width={provider.iconSize}
height={provider.iconSize}
/>
</div>
</div>
<span className="text-body1_m_16 text-black">
{provider.name}
</span>
</div>

{isConnected ? (
<div className="flex items-center gap-5">
<span className="text-body3_r_16 text-color-neutral-70">
연결됨
</span>
<button
onClick={() => handleDisconnect(provider.id)}
disabled={loadingProvider === provider.id}
className="text-primary border-primary-light text-sub3_sb_16 h-[46px] rounded-xl border-[1.4px] bg-white px-5 py-[13px] disabled:opacity-50"
>
연결해제
</button>
</div>
) : (
<button
onClick={() => handleConnect(provider.id)}
className="bg-primary text-sub3_sb_16 h-[46px] rounded-xl px-5 py-[13px] text-white"
>
연결하기
</button>
)}
</div>
)
})}
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use client'

import { UniversitySearchInput } from '@/components/UniversitySearchInput'
import { useSettingsContext } from './context'

export function CollegeSection() {
const {
isLoading,
defaultProfileValues,
collegeState: { collegeValue, setCollegeValue },
majorState: { setMajorValue }
} = useSettingsContext()

const effectiveValue =
(collegeValue && collegeValue !== 'none' ? collegeValue : null) ||
(defaultProfileValues.college && defaultProfileValues.college !== 'none'
? defaultProfileValues.college
: null) ||
''

return (
<div className="flex flex-col gap-1">
<label className="text-caption2_m_12 text-color-neutral-15">대학교</label>
<UniversitySearchInput
key={effectiveValue}
value={effectiveValue}
onChange={(v) => {
setCollegeValue(v)
setMajorValue('')
}}
excludeNames={['성균관대학교']}
placeholder={isLoading ? 'Loading...' : '대학교 검색'}
inputClassName="text-body1_m_16 text-color-neutral-30 placeholder:text-color-neutral-90"
/>
</div>
)
}

This file was deleted.

Loading