Compare commits

...

13 Commits

Author SHA1 Message Date
68408eb3c9 Merge branch 'dev' of https://git.hanasys.jp/qcast3/onsitesurvey into feature/survey 2025-05-07 18:09:51 +09:00
115ffb8a74 feat: set SurveyList Filtering
- 조사 매물 목록 페이지 필터링 구현
- 기존 survey-sales 샘플 페이지 모두 삭제
2025-05-07 18:06:10 +09:00
fd27bfe7d0 feat: implement create & update survey roof-information with validate
- 조사 매물 지붕 정보 필수값 적용하여 등록 & 수정 가능하도록 구현
- 조사 매물 상세 & 작성 페이지 Nav 탭 기본 정보 미등록 시 라우팅 안되도록 설정정
2025-05-07 18:05:28 +09:00
692d2b7d84 refactor: comment out login redirection in middleware for future implementation 2025-05-07 17:37:09 +09:00
05f4fe4e94 refactor: enhance session management and routing in middleware and API authentication 2025-05-07 17:16:26 +09:00
5048fe1c08 chore: axios 설정 충돌 해결 2025-05-07 15:17:01 +09:00
c9378dfccd Merge pull request 'snake case -> camel case 변환 로직 추가' (#25) from feature/pub into dev
Reviewed-on: #25
2025-05-07 15:11:04 +09:00
김민식
2166b7836e snake case -> camel case 변환 로직 추가 2025-05-07 15:08:33 +09:00
d340e3d976 Merge pull request 'refactor: remove suitable and surveySales API modules to streamline codebase' (#24) from feature/chore into dev
Reviewed-on: #24
2025-05-07 10:19:02 +09:00
82e198836a refactor: remove suitable and surveySales API modules to streamline codebase 2025-05-07 10:18:28 +09:00
daf9f31733 Merge branch 'dev' of https://git.hanasys.jp/qcast3/onsitesurvey into feature/survey 2025-05-07 09:00:35 +09:00
3cf55e1a9e Merge pull request 'refactor: enhance Header component by organizing useEffect hooks and updating client URL pathname handling' (#23) from feature/pub-yoo into dev
Reviewed-on: #23
2025-05-02 18:47:03 +09:00
3937179279 refactor: enhance Header component by organizing useEffect hooks and updating client URL pathname handling 2025-05-02 18:46:38 +09:00
50 changed files with 1635 additions and 1352 deletions

View File

@ -1,3 +1,7 @@
# 모바일 디바이스로 로컬 서버 확인하려면 자신 IP 주소로 변경
# 다시 로컬에서 개발할때는 localhost로 변경
NEXT_PUBLIC_API_URL=http://localhost:3000
#route handler
NEXT_PUBLIC_API_URL=http://localhost:3000
#qsp 로그인 api
NEXT_PUBLIC_QSP_API_URL=http://1.248.227.176:8120

View File

@ -1 +1,5 @@
NEXT_PUBLIC_API_URL=http://172.30.1.35:3000
#route handler
NEXT_PUBLIC_API_URL=http://172.30.1.35:3000
#qsp 로그인 api
NEXT_PUBLIC_QSP_API_URL=http://1.248.227.176:8120

View File

@ -6,14 +6,18 @@ const nextConfig: NextConfig = {
sassOptions: {
includePaths: [path.join(__dirname, './src/styles')],
},
// async rewrites() {
// return [
// {
// source: '/api/:path*',
// destination: `${process.env.NEXT_PUBLIC_API_URL}/api/:path*`,
// },
// ]
// },
async rewrites() {
return [
{
source: '/api/user/login',
destination: `${process.env.NEXT_PUBLIC_QSP_API_URL}/api/user/login`,
},
{
source: '/api/:path*',
destination: `${process.env.NEXT_PUBLIC_API_URL}/api/:path*`,
},
]
},
}
export default nextConfig

View File

@ -133,7 +133,7 @@ model SD_SERVEY_SALES_DETAIL_INFO {
//전기 소매 회사
retail_company String? @db.VarChar(100)
//전기 부대 설비
supplementary_facilities Int? @db.Int
supplementary_facilities String? @db.VarChar(20)
//전기 부대 설비 기타
supplementary_facilities_etc String? @db.VarChar(200)
//설치 희망 시스템
@ -145,7 +145,7 @@ model SD_SERVEY_SALES_DETAIL_INFO {
//건축 연수 기타
construction_year_etc String? @db.VarChar(200)
//지붕재
roof_material Int? @db.Int
roof_material String? @db.VarChar(20)
//지붕재 기타
roof_material_etc String? @db.VarChar(200)
//지붕 모양

View File

@ -1,95 +0,0 @@
import { database } from '@/data'
import { axiosInstance } from '@/libs/axios'
export interface Suitable {
id?: number
product_name: string
manufacturer: string
roof_material: string
shape: string
support_roof_tile: string
support_roof_tile_memo: string
support_roof_bracket: string
support_roof_bracket_memo: string
yg_anchor: string
yg_anchor_memo: string
rg_roof_tile_part: string
rg_roof_tile_part_memo: string
dido_hunt_support_tile_2: string
dido_hunt_support_tile_2_memo: string
takashima_power_base: string
takashima_power_base_memo: string
takashima_tile_bracket: string
takashima_tile_bracket_memo: string
slate_bracket_4: string
slate_bracket_4_memo: string
slate_single_metal_bracket: string
slate_single_metal_bracket_memo: string
dido_hunt_short_rack_4: string
dido_hunt_short_rack_4_memo: string
takashima_slate_bracket_slate_single: string
takashima_slate_bracket_slate_single_memo: string
df_metal_bracket: string
df_metal_bracket_memo: string
slate_metal_bracket: string
slate_metal_bracket_memo: string
takashima_slate_bracket_metal_roof: string
takashima_slate_bracket_metal_roof_memo: string
}
export const suitableApi = {
getList: async (): Promise<Suitable[]> => {
const response = await axiosInstance.get<Suitable[]>('/api/suitable/list')
console.log('🚀 ~ getList: ~ response:', response)
return response.data
},
getDetails: async (roofMaterial: string): Promise<Suitable[]> => {
const response = await axiosInstance.get<Suitable[]>(`/api/suitable/details?roof-material=${roofMaterial}`)
return response.data
},
create: async () => {
const suitableData: Suitable[] = []
database.forEach((item) => {
suitableData.push({
product_name: item[0],
manufacturer: item[1],
roof_material: item[2],
shape: item[3],
support_roof_tile: item[4],
support_roof_tile_memo: item[5],
support_roof_bracket: item[6],
support_roof_bracket_memo: item[7],
yg_anchor: item[8],
yg_anchor_memo: item[9],
rg_roof_tile_part: item[10],
rg_roof_tile_part_memo: item[11],
dido_hunt_support_tile_2: item[12],
dido_hunt_support_tile_2_memo: item[13],
takashima_power_base: item[14],
takashima_power_base_memo: item[15],
takashima_tile_bracket: item[16],
takashima_tile_bracket_memo: item[17],
slate_bracket_4: item[18],
slate_bracket_4_memo: item[19],
slate_single_metal_bracket: item[20],
slate_single_metal_bracket_memo: item[21],
dido_hunt_short_rack_4: item[22],
dido_hunt_short_rack_4_memo: item[23],
takashima_slate_bracket_slate_single: item[24],
takashima_slate_bracket_slate_single_memo: item[25],
df_metal_bracket: item[26],
df_metal_bracket_memo: item[27],
slate_metal_bracket: item[28],
slate_metal_bracket_memo: item[29],
takashima_slate_bracket_metal_roof: item[30],
takashima_slate_bracket_metal_roof_memo: item[31],
})
})
const response = await axiosInstance.post<Suitable[]>('/api/suitable', suitableData)
return response.data
},
}

View File

@ -1,5 +1,5 @@
import Header from '@/components/ui/common/Header'
export default function page() {
export default async function page() {
return <Header name={'調査物件一覧'} />
}

View File

@ -0,0 +1,15 @@
import { sessionOptions } from '@/libs/session'
import { SessionData } from '@/types/Auth'
import { getIronSession } from 'iron-session'
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'
export async function GET(request: Request) {
const cookieStore = await cookies()
const session = await getIronSession<SessionData>(cookieStore, sessionOptions)
session.destroy()
// return redirect('/login')
return NextResponse.json({ code: 200, message: 'Logout successful' })
}

58
src/app/api/auth/route.ts Normal file
View File

@ -0,0 +1,58 @@
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'
import { getIronSession } from 'iron-session'
import { axiosInstance } from '@/libs/axios'
import { sessionOptions } from '@/libs/session'
import type { SessionData } from '@/types/Auth'
export async function POST(request: Request) {
const { loginId, pwd } = await request.json()
const result = await axiosInstance(`${process.env.NEXT_PUBLIC_QSP_API_URL}`).post(`/api/user/login`, {
loginId,
pwd,
})
console.log('🚀 ~ result ~ result:', result)
if (result.data.result.code === 200) {
const cookieStore = await cookies()
const session = await getIronSession<SessionData>(cookieStore, sessionOptions)
console.log('start session edit!')
session.langCd = result.data.data.langCd
session.currPage = result.data.data.currPage
session.rowCount = result.data.data.rowCount
session.startRow = result.data.data.startRow
session.endRow = result.data.data.endRow
session.compCd = result.data.data.compCd
session.agencyStoreId = result.data.data.agencyStoreId
session.storeId = result.data.data.storeId
session.userId = result.data.data.userId
session.category = result.data.data.category
session.userNm = result.data.data.userNm
session.userNmKana = result.data.data.userNmKana
session.telNo = result.data.data.telNo
session.fax = result.data.data.fax
session.email = result.data.data.email
session.lastEditUser = result.data.data.lastEditUser
session.storeGubun = result.data.data.storeGubun
session.pwCurr = result.data.data.pwCurr
session.pwdInitYn = result.data.data.pwdInitYn
session.apprStatCd = result.data.data.apprStatCd
session.loginFailCnt = result.data.data.loginFailCnt
session.loginFailMinYn = result.data.data.loginFailMinYn
session.priceViewStatCd = result.data.data.priceViewStatCd
session.groupId = result.data.data.groupId
session.storeLvl = result.data.data.storeLvl
session.custCd = result.data.data.custCd
session.builderNo = result.data.data.builderNo
session.isLoggedIn = true
console.log('end session edit!')
await session.save()
}
return NextResponse.json({ code: 200, message: 'Login is Succecss!!' })
}

View File

@ -0,0 +1,16 @@
import { NextResponse } from 'next/server'
import { prisma } from '@/libs/prisma'
export async function GET() {
// @ts-ignore
const roofMaterialCategory = await prisma.MS_SUITABLE.findMany({
select: {
roof_material: true,
},
distinct: ['roof_material'],
orderBy: {
roof_material: 'asc',
},
})
return NextResponse.json(roofMaterialCategory)
}

View File

@ -0,0 +1,17 @@
import { NextResponse } from 'next/server'
import { prisma } from '@/libs/prisma'
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const roofMaterial = searchParams.get('roof-material')
console.log('🚀 ~ GET ~ roof-material:', roofMaterial)
// @ts-ignore
const suitables = await prisma.MS_SUITABLE.findMany({
where: {
roof_material: roofMaterial,
},
})
return NextResponse.json(suitables)
}

View File

@ -0,0 +1,34 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/libs/prisma'
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams
const category = searchParams.get('category')
const keyword = searchParams.get('keyword')
let whereCondition: any = {}
if (category) {
whereCondition['roof_material'] = category
}
if (keyword) {
whereCondition['product_name'] = {
contains: keyword,
}
}
console.log('🚀 ~ /api/suitable/list: ~ prisma where condition:', whereCondition)
// @ts-ignore
const suitables = await prisma.MS_SUITABLE.findMany({
where: whereCondition,
orderBy: {
product_name: 'asc',
},
})
return NextResponse.json(suitables)
} catch (error) {
console.error('❌ 데이터 조회 중 오류가 발생했습니다:', error)
return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 })
}
}

View File

@ -0,0 +1,12 @@
import { NextResponse } from 'next/server'
import { prisma } from '@/libs/prisma'
export async function POST(request: Request) {
const body = await request.json()
// @ts-ignore
const suitables = await prisma.MS_SUITABLE.createMany({
data: body,
})
return NextResponse.json({ message: 'Suitable created successfully' })
}

View File

@ -0,0 +1,72 @@
import { NextResponse } from 'next/server'
export async function POST(request: Request, context: { params: { id: string } }) {
const body = await request.json()
const { id } = await context.params
// @ts-ignore
const survey = await prisma.SD_SERVEY_SALES_BASIC_INFO.update({
where: { id: Number(id) },
data: {
detail_info: {
create: body,
},
},
})
return NextResponse.json({ message: 'Survey detail created successfully' })
}
export async function GET(request: Request, context: { params: { id: string } }) {
const { id } = await context.params
// @ts-ignore
const survey = await prisma.SD_SERVEY_SALES_BASIC_INFO.findUnique({
where: { id: Number(id) },
include: {
detail_info: true,
},
})
return NextResponse.json(survey)
}
export async function PUT(request: Request, context: { params: { id: string } }) {
const { id } = await context.params
const body = await request.json()
// @ts-ignore
const survey = await prisma.SD_SERVEY_SALES_BASIC_INFO.update({
where: { id: Number(id) },
data: {
...body,
detail_info: {
update: body.detail_info,
},
},
})
return NextResponse.json(survey)
}
export async function DELETE(request: Request, context: { params: { id: string; detail_id: string } }) {
const { id, detail_id } = await context.params
if (detail_id) {
// @ts-ignore
const survey = await prisma.SD_SERVEY_SALES_DETAIL_INFO.delete({
where: { id: Number(detail_id) },
})
} else {
// @ts-ignore
const survey = await prisma.SD_SERVEY_SALES_BASIC_INFO.delete({
where: { id: Number(id) },
})
}
return NextResponse.json({ message: 'Survey deleted successfully' })
}
export async function PATCH(request: Request, context: { params: { id: string } }) {
const { id } = await context.params
// @ts-ignore
const survey = await prisma.SD_SERVEY_SALES_BASIC_INFO.update({
where: { id: Number(id) },
data: {
submission_status: true,
},
})
return NextResponse.json({ message: 'Survey confirmed successfully' })
}

View File

@ -0,0 +1,33 @@
import { NextResponse } from 'next/server'
import { prisma } from '@/libs/prisma'
export async function POST(request: Request) {
const body = await request.json()
// @ts-ignore
const res = await prisma.SD_SERVEY_SALES_BASIC_INFO.create({
data: body,
})
return NextResponse.json(res)
}
export async function GET() {
try {
// @ts-ignore
const res = await prisma.SD_SERVEY_SALES_BASIC_INFO.findMany()
return NextResponse.json(res)
} catch (error) {
console.error(error)
}
}
export async function PUT(request: Request) {
const body = await request.json()
console.log('🚀 ~ PUT ~ body:', body)
const detailInfo = { ...body.detail_info, basic_info_id: body.id }
console.log('🚀 ~ PUT ~ detailInfo:', detailInfo)
// @ts-ignore
const res = await prisma.SD_SERVEY_SALES_DETAIL_INFO.create({
data: detailInfo,
})
return NextResponse.json({ message: 'Survey sales updated successfully' })
}

View File

@ -0,0 +1,23 @@
import { NextResponse } from 'next/server'
import { prisma } from '@/libs/prisma'
export async function POST(request: Request) {
try {
const body = await request.json()
const { username, email, password } = body
const user = await prisma.user.create({
data: {
username,
email,
password,
updated_at: new Date(),
},
})
return NextResponse.json(user)
} catch (error) {
console.error('Error creating user:', error)
return NextResponse.json({ error: 'Error creating user' }, { status: 500 })
}
}

View File

@ -0,0 +1,7 @@
import { NextResponse } from 'next/server'
import { prisma } from '@/libs/prisma'
export const GET = async () => {
const users = await prisma.user.findMany()
return NextResponse.json(users)
}

37
src/app/api/user/route.ts Normal file
View File

@ -0,0 +1,37 @@
import { NextResponse } from 'next/server'
import { prisma } from '@/libs/prisma'
import { getIronSession } from 'iron-session'
import { cookies } from 'next/headers'
import { SessionData, sessionOptions } from '@/libs/session'
export async function POST(request: Request) {
const { username, password } = await request.json()
console.log('🚀 ~ POST ~ username:', username)
console.log('🚀 ~ POST ~ password:', password)
const user = await prisma.user.findFirst({
where: {
username: username,
password: password,
},
})
console.log('🚀 ~ POST ~ user:', user)
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 })
}
const cookieStore = await cookies()
const session = await getIronSession<SessionData>(cookieStore, sessionOptions)
console.log('start session edit!')
session.username = user.username!
session.email = user.email!
session.isLoggedIn = true
console.log('end session edit!')
await session.save()
console.log('🚀 ~ POST ~ session:', session)
// return NextResponse.redirect(new URL(process.env.NEXT_PUBLIC_URL!, request.url))
return NextResponse.json(user)
}

View File

@ -1,6 +1,14 @@
import Main from '@/components/ui/Main'
import { sessionOptions } from '@/libs/session'
import type { SessionData } from '@/types/Auth'
import { getIronSession } from 'iron-session'
import { cookies } from 'next/headers'
export default async function Home() {
const cookieStore = await cookies()
const session = await getIronSession<SessionData>(cookieStore, sessionOptions)
console.log('🚀 ~ Home ~ session:', session)
return (
<>
<div className="container">

View File

@ -1,4 +1,4 @@
import BasicForm from '@/components/survey-sale/detail/BasicForm'
import BasicForm from '@/components/survey-sale/detail/form/BasicForm'
export default function page() {
return (

View File

@ -4,7 +4,6 @@ import SearchForm from '@/components/survey-sale/list/SearchForm'
export default function page() {
return (
<>
<SearchForm />
<ListTable />
</>
)

View File

@ -1,4 +1,4 @@
import RoofInfoForm from '@/components/survey-sale/detail/RoofInfoForm'
import RoofInfoForm from '@/components/survey-sale/detail/form/RoofInfoForm'
export default function page() {
return (

View File

@ -1,16 +1,69 @@
'use client'
import { useState } from 'react'
import { useReducer, useState } from 'react'
import { useRouter } from 'next/navigation'
import { useQuery } from '@tanstack/react-query'
import { axiosInstance } from '@/libs/axios'
interface AccountState {
loginId: string
pwd: string
}
export default function Login() {
const [pwShow, setPwShow] = useState(false) //비밀번호 보이기 숨기기
const router = useRouter()
//비밀번호 보이기 숨기기
const [pwShow, setPwShow] = useState(false)
//ID 저장 체크박스
const [idSave, setIdSave] = useState(false)
//Q.PARTNERS 체크박스
const [isPartners, setIsPartners] = useState(false)
//로그인 상태
const [isLogin, setIsLogin] = useState(false)
const reducer = (state: AccountState, newState: Partial<AccountState>) => ({ ...state, ...newState })
const [account, setAccount] = useReducer(reducer, {
loginId: '',
pwd: '',
})
interface LoginData {
code: number
message: string | null
}
const {
data: loginData,
isPending,
error,
} = useQuery<LoginData, Error>({
queryKey: ['login', 'account'],
queryFn: async () => {
const { data } = await axiosInstance('').post<LoginData>(`/api/auth`, {
loginId: account.loginId,
pwd: account.pwd,
})
router.push('/')
return data
},
enabled: isLogin,
staleTime: 0,
retry: false,
})
return (
<>
<div className="login-form-wrap">
<div className="login-form mb15">
<div className="login-input id">
<input type="text" className="login-frame" placeholder="Input Frame ID" />
<input
type="text"
className="login-frame"
placeholder="Input Frame ID"
value={account.loginId}
onChange={(e) => setAccount({ loginId: e.target.value })}
/>
<button className="login-icon">
<i className="del-icon"></i>
</button>
@ -18,7 +71,13 @@ export default function Login() {
</div>
<div className="login-form">
<div className="login-input pw">
<input type={`${pwShow ? 'text' : 'password'}`} className="login-frame" placeholder="Input Frame PW" />
<input
type={`${pwShow ? 'text' : 'password'}`}
className="login-frame"
placeholder="Input Frame PW"
value={account.pwd}
onChange={(e) => setAccount({ pwd: e.target.value })}
/>
<button className={`login-icon ${pwShow ? 'act' : ''}`} onClick={() => setPwShow(!pwShow)}>
<i className="show-icon"></i>
</button>
@ -26,19 +85,19 @@ export default function Login() {
</div>
<div className="login-check-warp">
<div className="check-form-box">
<input type="checkbox" id="ch01" />
<input type="checkbox" id="ch01" checked={idSave} onChange={() => setIdSave(!idSave)} />
<label htmlFor="ch01">ID Save</label>
</div>
<div className="toggle-form">
<label className="toggle-btn">
<input type="checkbox" />
<input type="checkbox" checked={isPartners} onChange={() => setIsPartners(!isPartners)} />
<span className="slider"></span>
</label>
<div className="toggle-name">Q.PARTNERS</div>
</div>
</div>
<div className="login-btn-wrap">
<button className="btn-frame icon login">
<button className="btn-frame icon login" onClick={() => setIsLogin(true)}>
<i className="btn-arr"></i>
</button>
</div>

View File

@ -1,16 +1,19 @@
'use client'
import { useSurveySaleTabState } from '@/store/surveySaleTabState'
import { usePathname, useRouter } from 'next/navigation'
import { usePathname, useRouter, useSearchParams, useParams } from 'next/navigation'
import { useEffect } from 'react'
import { usePopupController } from '@/store/popupController'
export default function NavTab() {
const router = useRouter()
const pathname = usePathname()
if (pathname === '/survey-sale') {
return null
}
const searchParams = useSearchParams()
const id = searchParams.get('id')
const params = useParams()
const detailId = params.id
const { basicInfoSelected, roofInfoSelected, reset } = useSurveySaleTabState()
@ -18,14 +21,37 @@ export default function NavTab() {
return () => {
reset()
}
}, [])
}, [reset])
if (pathname === '/survey-sale') {
return null
}
const handleBasicInfoClick = () => {
router.push('/survey-sale/basic-info')
if (id) {
router.push(`/survey-sale/basic-info?id=${id}`)
return
}
if (detailId) {
router.push(`/survey-sale/${detailId}?tab=basic-info`)
return
}
}
const handleRoofInfoClick = () => {
router.push('/survey-sale/roof-info')
if (id) {
router.push(`/survey-sale/roof-info?id=${id}`)
return
}
if (detailId) {
router.push(`/survey-sale/${detailId}?tab=roof-info`)
return
}
if (pathname === '/survey-sale/basic-info') {
// TODO: 팝업 추가
alert('Save essential information first')
return null
}
}
return (

View File

@ -2,12 +2,21 @@
import { useServey } from '@/hooks/useSurvey'
import { useParams } from 'next/navigation'
import { useEffect } from 'react'
import { useState } from 'react'
export default function DataTable() {
const params = useParams()
const id = params.id
const { surveyDetail, isLoadingSurveyDetail } = useServey(Number(id))
const [isTemporary, setIsTemporary] = useState(false)
useEffect(() => {
if (!surveyDetail?.representative || !surveyDetail?.store || !surveyDetail?.construction_point) {
setIsTemporary(true)
}
}, [surveyDetail])
if (isLoadingSurveyDetail) {
return <div>Loading...</div>
@ -24,7 +33,13 @@ export default function DataTable() {
<tbody>
<tr>
<th></th>
<td>{surveyDetail?.id}</td>
{isTemporary ? (
<td>
<span className="text-red-500"></span>
</td>
) : (
<td>{surveyDetail?.id}</td>
)}
</tr>
<tr>
<th></th>
@ -39,7 +54,7 @@ export default function DataTable() {
<td>
{surveyDetail?.submission_status && surveyDetail?.submission_date
? new Date(surveyDetail.submission_date).toLocaleString()
: '未提出'}
: '-'}
</td>
</tr>
<tr>

View File

@ -1,15 +1,37 @@
'use client'
import { useServey } from '@/hooks/useSurvey'
import { useParams, useRouter } from 'next/navigation'
import { useSurveySaleTabState } from '@/store/surveySaleTabState'
import { useEffect, useState } from 'react'
import { useParams, useRouter, useSearchParams } from 'next/navigation'
export default function DetailForm() {
const router = useRouter()
const params = useParams()
const id = params.id
const { surveyDetail, deleteSurvey, submitSurvey, isLoadingSurveyDetail } = useServey(Number(id))
const searchParams = useSearchParams()
const tab = searchParams.get('tab')
const { surveyDetail, deleteSurvey, submitSurvey, isLoadingSurveyDetail } = useServey(Number(id))
const { setBasicInfoSelected, setRoofInfoSelected } = useSurveySaleTabState()
const [isTemporary, setIsTemporary] = useState(true)
// TODO: 조사매물 지붕정보 퍼블 구현되면 탭 화면 변경 추가
useEffect(() => {
if (tab === 'basic-info') {
setBasicInfoSelected()
}
if (tab === 'roof-info') {
setRoofInfoSelected()
}
if (surveyDetail?.representative && surveyDetail?.store && surveyDetail?.construction_point) {
setIsTemporary(false)
}
}, [tab, setBasicInfoSelected, setRoofInfoSelected, surveyDetail])
console.log('isTemporary', isTemporary)
console.log('surveyDetail', surveyDetail)
if (isLoadingSurveyDetail) {
return <div>Loading...</div>
}
@ -68,16 +90,22 @@ export default function DetailForm() {
</div>
</div>
<div className="btn-flex-wrap">
<div className="btn-bx">
<button className="btn-frame n-blue icon" onClick={() => router.push('/survey-sale')}>
<i className="btn-arr"></i>
</button>
</div>
<div className="btn-bx">
<button className="btn-frame red icon" onClick={handleSubmit}>
<i className="btn-arr"></i>
</button>
</div>
{isTemporary ? (
<></>
) : (
<>
<div className="btn-bx">
<button className="btn-frame n-blue icon" onClick={() => router.push('/survey-sale')}>
<i className="btn-arr"></i>
</button>
</div>
<div className="btn-bx">
<button className="btn-frame red icon" onClick={handleSubmit}>
<i className="btn-arr"></i>
</button>
</div>
</>
)}
<div className="btn-bx">
<button className="btn-frame n-blue icon" onClick={handleUpdate}>
<i className="btn-arr"></i>

View File

@ -1,415 +0,0 @@
'use client'
import { useSurveySaleTabState } from '@/store/surveySaleTabState'
import { useServey } from '@/hooks/useSurvey'
import { SurveyDetailRequest } from '@/types/Survey'
import { useRouter, useSearchParams } from 'next/navigation'
import { useEffect, useState } from 'react'
import MultiCheckbox from './form/MultiCheckbox'
const defaultDetailInfoForm: SurveyDetailRequest = {
contract_capacity: null,
retail_company: null,
supplementary_facilities: null,
supplementary_facilities_etc: null,
installation_system: null,
installation_system_etc: null,
construction_year: null,
construction_year_etc: null,
roof_material: null,
roof_material_etc: null,
roof_shape: null,
roof_shape_etc: null,
roof_slope: null,
house_structure: null,
house_structure_etc: null,
rafter_material: null,
rafter_material_etc: null,
rafter_size: null,
rafter_size_etc: null,
rafter_pitch: null,
rafter_pitch_etc: null,
rafter_direction: null,
open_field_plate_kind: null,
open_field_plate_kind_etc: null,
open_field_plate_thickness: null,
leak_trace: null,
waterproof_material: null,
waterproof_material_etc: null,
insulation_presence: null,
insulation_presence_etc: null,
structure_order: null,
structure_order_etc: null,
installation_availability: null,
installation_availability_etc: null,
memo: null,
}
export default function RoofInfoForm() {
const { setRoofInfoSelected } = useSurveySaleTabState()
useEffect(() => {
setRoofInfoSelected()
}, [])
const router = useRouter()
const searchParams = useSearchParams()
const id = searchParams.get('id')
const { updateSurvey, isUpdatingSurvey, surveyDetail, createSurveyDetail } = useServey(Number(id))
const [detailInfoData, setDetailInfoData] = useState<SurveyDetailRequest>(defaultDetailInfoForm)
useEffect(() => {
if (surveyDetail?.detail_info) {
const { id, updated_at, created_at, ...rest } = surveyDetail.detail_info
setDetailInfoData(rest)
}
}, [surveyDetail])
const handleNumberInput = (key: keyof SurveyDetailRequest, value: number | string) => {
if (typeof value === 'string') {
const numberValue = value === '' ? null : Number(value)
setDetailInfoData({ ...detailInfoData, [key]: numberValue })
} else {
setDetailInfoData({ ...detailInfoData, [key]: value })
}
}
const handleTextInput = (key: keyof SurveyDetailRequest, value: string) => {
setDetailInfoData({ ...detailInfoData, [key]: value || null })
}
const handleBooleanInput = (key: keyof SurveyDetailRequest, checked: boolean) => {
setDetailInfoData({ ...detailInfoData, [key]: checked })
}
const handleUnitInput = (value: string) => {
const capacity = detailInfoData.contract_capacity
setDetailInfoData({ ...detailInfoData, contract_capacity: `${capacity} ${value}` })
}
const handleSave = () => {
if (id) {
console.log('detailInfoData:: ', detailInfoData)
}
}
return (
<>
<div className="sale-frame">
<div className="sale-roof-title"></div>
<div className="data-form-wrap">
<div className="data-input-form-bx">
<div className="data-input-form-tit"></div>
<div className="data-input mb5">
<input
type="text"
className="input-frame"
value={detailInfoData.contract_capacity ?? ''}
onChange={(e) => handleTextInput('contract_capacity', e.target.value)}
/>
</div>
<div className="data-input">
<select
className="select-form"
name="contract_capacity_unit"
id="contract_capacity_unit"
onChange={(e) => handleUnitInput(e.target.value)}
>
<option value="kVA">kVA</option>
<option value="">kVA</option>
<option value="">kVA</option>
<option value="">kVA</option>
<option value="">kVA</option>
</select>
</div>
</div>
<div className="data-input-form-bx">
<div className="data-input-form-tit"></div>
<input
type="text"
className="input-frame"
value={detailInfoData.retail_company ?? ''}
onChange={(e) => handleTextInput('retail_company', e.target.value)}
/>
</div>
<div className="data-input-form-bx">
<MultiCheckbox column={'supplementary_facilities'} setDetailInfoData={setDetailInfoData} detailInfoData={detailInfoData}/>
</div>
<div className="data-input-form-bx">
<div className="data-input-form-tit red-f"></div>
<div className="data-input mb5">
<select className="select-form" name="" id="">
<option value=""></option>
<option value=""></option>
<option value=""></option>
<option value=""></option>
<option value=""></option>
</select>
</div>
<div className="data-input">
<input type="text" className="input-frame" defaultValue={''} disabled />
</div>
</div>
</div>
</div>
<div className="sale-frame">
<div className="sale-roof-title"></div>
<div className="data-form-wrap">
<div className="data-input-form-bx">
<div className="data-input-form-tit red-f"></div>
<div className="data-input mb5">
<select className="select-form" name="" id="">
<option value=""></option>
<option value=""></option>
<option value=""></option>
<option value=""></option>
<option value=""></option>
</select>
</div>
<div className="data-input flex">
<input type="text" className="input-frame" defaultValue={''} disabled />
<span></span>
</div>
</div>
<div className="data-input-form-bx">
<MultiCheckbox column={'roof_material'} setDetailInfoData={setDetailInfoData} detailInfoData={detailInfoData} />
</div>
<div className="data-input-form-bx">
<div className="data-input-form-tit"></div>
<div className="data-input mb5">
<select className="select-form" name="" id="">
<option value=""></option>
<option value=""></option>
<option value=""></option>
<option value=""></option>
<option value=""></option>
</select>
</div>
<div className="data-input">
<input type="text" className="input-frame" defaultValue={''} disabled />
</div>
</div>
<div className="data-input-form-bx">
<div className="data-input-form-tit"></div>
<div className="data-input flex">
<input type="text" className="input-frame" defaultValue={'4'} />
<span></span>
</div>
</div>
<div className="data-input-form-bx">
<div className="data-input-form-tit"></div>
<div className="radio-form-box mb10">
<input type="radio" name="radio01" id="ra01" />
<label htmlFor="ra01"></label>
</div>
<div className="radio-form-box mb10">
<input type="radio" name="radio01" id="ra02" />
<label htmlFor="ra02"> ()</label>
</div>
<div className="data-input">
<input type="text" className="input-frame" disabled defaultValue={''} />
</div>
</div>
<div className="data-input-form-bx">
<div className="data-input-form-tit"></div>
<div className="data-check-wrap">
<div className="radio-form-box">
<input type="radio" name="radio02" id="ra03" />
<label htmlFor="ra03"></label>
</div>
<div className="radio-form-box">
<input type="radio" name="radio02" id="ra04" />
<label htmlFor="ra04"></label>
</div>
<div className="radio-form-box">
<input type="radio" name="radio02" id="ra05" />
<label htmlFor="ra05"> ()</label>
</div>
</div>
<div className="data-input">
<input type="text" className="input-frame" disabled defaultValue={''} />
</div>
</div>
<div className="data-input-form-bx">
<div className="data-input-form-tit red-f"></div>
<div className="data-input mb5">
<select className="select-form" name="" id="">
<option value="">35mm以上×48mm以上</option>
<option value="">35mm以上×48mm以上</option>
<option value="">35mm以上×48mm以上</option>
<option value="">35mm以上×48mm以上</option>
<option value="">35mm以上×48mm以上</option>
</select>
</div>
<div className="data-input">
<input type="text" className="input-frame" defaultValue={''} disabled />
</div>
</div>
<div className="data-input-form-bx">
<div className="data-input-form-tit red-f"></div>
<div className="data-input mb5">
<select className="select-form" name="" id="">
<option value="">455mm以下</option>
<option value="">455mm以下</option>
<option value="">455mm以下</option>
<option value="">455mm以下</option>
<option value="">455mm以下</option>
</select>
</div>
<div className="data-input">
<input type="text" className="input-frame" defaultValue={''} disabled />
</div>
</div>
<div className="data-input-form-bx">
<div className="data-input-form-tit red-f"></div>
<div className="data-check-wrap mb0">
<div className="radio-form-box">
<input type="radio" name="radio03" id="ra06" />
<label htmlFor="ra06"></label>
</div>
<div className="radio-form-box">
<input type="radio" name="radio03" id="ra07" />
<label htmlFor="ra07"></label>
</div>
</div>
</div>
<div className="data-input-form-bx">
<div className="data-input-form-tit"></div>
<div className="data-input mb5">
<select className="select-form" name="" id="">
<option value=""></option>
<option value=""></option>
<option value=""></option>
<option value=""></option>
<option value=""></option>
</select>
</div>
<div className="data-input">
<input type="text" className="input-frame" defaultValue={''} disabled />
</div>
</div>
<div className="data-input-form-bx">
<div className="data-input-form-tit">
<span>, . </span>
</div>
<div className="data-input flex">
<input type="text" className="input-frame" defaultValue={'150'} />
<span>mm</span>
</div>
</div>
<div className="data-input-form-bx">
<div className="data-input-form-tit "></div>
<div className="data-check-wrap mb0">
<div className="radio-form-box">
<input type="radio" name="radio04" id="ra08" />
<label htmlFor="ra08"></label>
</div>
<div className="radio-form-box">
<input type="radio" name="radio04" id="ra09" />
<label htmlFor="ra09"></label>
</div>
</div>
</div>
<div className="data-input-form-bx">
<div className="data-input-form-tit red-f"></div>
<div className="radio-form-box mb10">
<input type="radio" name="radio05" id="ra10" />
<label htmlFor="ra10">94022kg以上</label>
</div>
<div className="radio-form-box mb10">
<input type="radio" name="radio05" id="ra11" />
<label htmlFor="ra11"> ()</label>
</div>
<div className="data-input">
<input type="text" className="input-frame" disabled defaultValue={''} />
</div>
</div>
<div className="data-input-form-bx">
<div className="data-input-form-tit red-f"></div>
<div className="radio-form-box mb10">
<input type="radio" name="radio06" id="ra12" />
<label htmlFor="ra12"></label>
</div>
<div className="data-input mb10">
<input type="text" className="input-frame" defaultValue={''} />
</div>
<div className="radio-form-box">
<input type="radio" name="radio06" id="ra13" />
<label htmlFor="ra13"></label>
</div>
</div>
<div className="data-input-form-bx">
<div className="data-input-form-tit red-f"></div>
<div className="data-input mb5">
<select className="select-form" name="" id="">
<option value="">///</option>
<option value="">///</option>
<option value="">///</option>
<option value="">///</option>
<option value="">///</option>
</select>
</div>
<div className="data-input">
<input type="text" className="input-frame" defaultValue={''} disabled />
</div>
</div>
<div className="data-input-form-bx">
<div className="data-input-form-tit"> </div>
<div className="data-input mb5">
<select className="select-form" name="" id="">
<option value=""></option>
<option value=""></option>
<option value=""></option>
<option value=""></option>
<option value=""></option>
</select>
</div>
<div className="data-input">
<input type="text" className="input-frame" defaultValue={'高島'} />
</div>
</div>
<div className="data-input-form-bx">
<div className="data-input-form-tit"></div>
<div className="data-input mb5">
<select className="select-form" name="" id="">
<option value=""></option>
<option value=""></option>
<option value=""></option>
<option value=""></option>
<option value=""></option>
</select>
</div>
<div className="data-input">
<textarea
className="textarea-form"
name=""
id=""
defaultValue={'漏れの兆候があるため、正確な点検が必要です.'}
placeholder="TextArea Filed"
></textarea>
</div>
</div>
</div>
<div className="btn-flex-wrap">
<div className="btn-bx">
<button className="btn-frame n-blue icon" onClick={handleSave}>
<i className="btn-arr"></i>
</button>
</div>
<div className="btn-bx">
<button className="btn-frame red icon" onClick={handleSave}>
<i className="btn-arr"></i>
</button>
</div>
<div className="btn-bx">
<button className="btn-frame n-blue icon" onClick={() => router.push('/survey-sale')}>
<i className="btn-arr"></i>
</button>
</div>
</div>
</div>
</>
)
}

View File

@ -20,10 +20,14 @@ const defaultBasicInfoForm: SurveyBasicRequest = {
submission_date: null,
}
const REQUIRED_FIELDS: (keyof SurveyBasicRequest)[] = ['representative', 'store', 'construction_point']
export default function BasicForm() {
const searchParams = useSearchParams()
const id = searchParams.get('id')
const router = useRouter()
const { setBasicInfoSelected } = useSurveySaleTabState()
const { surveyDetail, createSurvey, isCreatingSurvey, updateSurvey, isUpdatingSurvey } = useServey(Number(id))
const [basicInfoData, setBasicInfoData] = useState<SurveyBasicRequest>(defaultBasicInfoForm)
@ -35,31 +39,53 @@ export default function BasicForm() {
}
}, [surveyDetail])
useEffect(() => {
setBasicInfoSelected()
}, [])
const focusInput = (input: keyof SurveyBasicRequest) => {
const inputElement = document.getElementById(input)
if (inputElement) {
inputElement.focus()
}
}
const validateSurvey = (basicInfoData: SurveyBasicRequest) => {
const emptyField = REQUIRED_FIELDS.find((field) => !basicInfoData[field])
if (emptyField) {
focusInput(emptyField)
return false
}
return true
}
const handleChange = (key: keyof SurveyBasicRequest, value: string) => {
setBasicInfoData({ ...basicInfoData, [key]: value })
}
const router = useRouter()
const handleSave = () => {
const handleSave = async (isTemporary: boolean) => {
if (id) {
console.log('basicInfoData:: ', basicInfoData)
updateSurvey(basicInfoData)
} else {
createSurvey(basicInfoData)
router.push(`/survey-sale/${id}?tab=basic-info`)
}
if (isTemporary) {
const saveId = await createSurvey(basicInfoData)
alert('save success temporary id: ' + saveId)
router.push(`/survey-sale/${saveId}?tab=basic-info`)
} else {
if (validateSurvey(basicInfoData)) {
const saveId = await createSurvey(basicInfoData)
alert('save success id: ' + saveId)
router.push(`/survey-sale/${saveId}?tab=basic-info`)
}
}
router.push('/survey-sale')
}
if (isCreatingSurvey || isUpdatingSurvey) {
return <div>Loading...</div>
}
const { setBasicInfoSelected } = useSurveySaleTabState()
useEffect(() => {
setBasicInfoSelected()
}, [])
return (
<>
<div className="sale-frame">
@ -72,6 +98,7 @@ export default function BasicForm() {
id="representative"
value={basicInfoData.representative}
onChange={(e) => handleChange('representative', e.target.value)}
required
/>
</div>
<div className="data-input-form-bx">
@ -82,6 +109,7 @@ export default function BasicForm() {
id="store"
value={basicInfoData.store ?? ''}
onChange={(e) => handleChange('store', e.target.value)}
required
/>
</div>
<div className="data-input-form-bx">
@ -92,6 +120,7 @@ export default function BasicForm() {
id="construction_point"
value={basicInfoData.construction_point ?? ''}
onChange={(e) => handleChange('construction_point', e.target.value)}
required
/>
</div>
</div>
@ -101,7 +130,6 @@ export default function BasicForm() {
<div className="data-form-wrap">
<div className="data-input-form-bx">
<div className="data-input-form-tit">調</div>
{/* TODO: 달력 라이브러리 추가 ?? */}
<div className="date-input">
<button className="date-btn">
<i className="date-icon"></i>
@ -110,7 +138,7 @@ export default function BasicForm() {
type="date"
className="date-frame"
id="investigation_date"
value={basicInfoData.investigation_date ?? ''}
value={basicInfoData.investigation_date ?? new Date().toISOString().split('T')[0]}
onChange={(e) => handleChange('investigation_date', e.target.value)}
/>
</div>
@ -182,12 +210,12 @@ export default function BasicForm() {
</div>
<div className="btn-flex-wrap">
<div className="btn-bx">
<button className="btn-frame n-blue icon" onClick={handleSave}>
<button className="btn-frame n-blue icon" onClick={() => handleSave(true)}>
<i className="btn-arr"></i>
</button>
</div>
<div className="btn-bx">
<button className="btn-frame red icon">
<button className="btn-frame red icon" onClick={() => handleSave(false)}>
<i className="btn-arr"></i>
</button>
</div>

View File

@ -0,0 +1,141 @@
import { SurveyDetailRequest } from '@/types/Survey'
import { useState } from 'react'
const supplementary_facilities = [
{ id: 1, name: 'エコキュート' }, //에코큐트
{ id: 2, name: 'エネパーム' }, //에네팜
{ id: 3, name: '蓄電池システム' }, //축전지시스템
{ id: 4, name: '太陽光発電' }, //태양광발전
]
const roof_material = [
{ id: 1, name: 'スレート' }, //슬레이트
{ id: 2, name: 'アスファルトシングル' }, //아스팔트 싱글
{ id: 3, name: '瓦' }, //기와
{ id: 4, name: '金属屋根' }, //금속지붕
]
export default function MultiCheckbox({
column,
setDetailInfoData,
detailInfoData,
}: {
column: string
setDetailInfoData: (data: any) => void
detailInfoData: SurveyDetailRequest
}) {
const selectList = column === 'supplementary_facilities' ? supplementary_facilities : roof_material
const [isOtherChecked, setIsOtherChecked] = useState(false)
const [otherValue, setOtherValue] = useState('')
const handleCheckbox = (dataIndex: number) => {
const value = String(detailInfoData[column as keyof SurveyDetailRequest] ?? '')
.split(',')
.map((v) => v.trim())
.filter((v) => v.length > 0)
let newValue: string[]
if (value.includes(String(dataIndex))) {
// 체크 해제
newValue = value.filter((v) => v !== String(dataIndex))
} else {
// 체크
if (column === 'roof_material') {
// 기타가 체크되어 있는지 확인
const isOtherSelected = isOtherChecked
// 현재 선택된 항목 수 + 기타 선택 여부
const totalSelected = value.length + (isOtherSelected ? 1 : 0)
if (totalSelected >= 2) {
alert('屋根材は最大2個まで選択可能です。')
return
}
}
newValue = [...value, String(dataIndex)]
}
setDetailInfoData({
...detailInfoData,
[column]: newValue.join(', '),
})
}
const handleOtherCheckbox = () => {
if (column === 'roof_material') {
const value = String(detailInfoData[column as keyof SurveyDetailRequest] ?? '')
.split(',')
.map((v) => v.trim())
.filter((v) => v.length > 0)
// 현재 선택된 항목 수
const currentSelected = value.length
if (!isOtherChecked && currentSelected >= 2) {
alert('Up to two roofing materials can be selected.')
return
}
}
setIsOtherChecked(!isOtherChecked)
if (!isOtherChecked) {
setOtherValue('')
setDetailInfoData({
...detailInfoData,
[`${column}_etc`]: null,
})
} else {
setOtherValue('')
}
}
const handleOtherInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
setOtherValue(value)
setDetailInfoData({
...detailInfoData,
[`${column}_etc`]: value,
})
}
return (
<>
{column === 'supplementary_facilities' ? (
<>
<div className="data-input-form-tit">
<span></span>
</div>
</>
) : (
<>
<div className="data-input-form-tit">
<span>2</span>
</div>
</>
)}
<div className="data-check-wrap">
{selectList.map((item) => (
<div className="check-form-box" key={item.id}>
<input
type="checkbox"
id={`${column}_ch${item.id}`}
checked={String(detailInfoData[column as keyof SurveyDetailRequest] ?? '')
.split(',')
.map((v) => v.trim())
.includes(String(item.id))}
onChange={() => handleCheckbox(item.id)}
/>
<label htmlFor={`${column}_ch${item.id}`}>{item.name}</label>
</div>
))}
<div className="check-form-box">
<input type="checkbox" id={`${column}_ch05`} checked={isOtherChecked} onChange={handleOtherCheckbox} />
<label htmlFor={`${column}_ch05`}> ()</label>
</div>
</div>
<div className="data-input">
<input type="text" className="input-frame" disabled={!isOtherChecked} value={otherValue} onChange={handleOtherInputChange} />
</div>
</>
)
}

View File

@ -1,81 +0,0 @@
import { SurveyDetailRequest } from '@/types/Survey'
import { useState } from 'react'
const supplementary_facilities = [
{ id: 1, name: 'エコキュート' },
{ id: 2, name: 'エネパーム' },
{ id: 3, name: '蓄電池システム' },
{ id: 4, name: '太陽光発電' },
]
const roof_material = [
{ id: 1, name: 'スレート' },
{ id: 2, name: 'アスファルトシングル' },
{ id: 3, name: '瓦' },
{ id: 4, name: '金属屋根' },
]
export default function MultiCheckbox({
column,
setDetailInfoData,
detailInfoData,
}: {
column: string
setDetailInfoData: (data: any) => void
detailInfoData: SurveyDetailRequest
}) {
const selectList = column === 'supplementary_facilities' ? supplementary_facilities : roof_material
const [isOtherChecked, setIsOtherChecked] = useState(false)
const handleCheckbox = (dataName: string) => {
const value = column === 'supplementary_facilities' ? detailInfoData.supplementary_facilities : detailInfoData.roof_material
setDetailInfoData({
...detailInfoData,
[column]: `${value}, ${dataName}`,
})
}
return (
<>
{column === 'supplementary_facilities' ? (
<>
<div className="data-input-form-tit">
<span></span>
</div>
</>
) : (
<>
<div className="data-input-form-tit">
<span>2</span>
</div>
</>
)}
<div className="data-check-wrap">
{selectList.map((item) => (
<div className="check-form-box" key={item.id}>
<input
type="checkbox"
id={`ch${item.id}`}
checked={
String(detailInfoData[column as keyof SurveyDetailRequest] ?? '')
.split(',')
.map((v) => v.trim())
.includes(item.name)
}
onChange={() => handleCheckbox(item.name)}
/>
<label htmlFor={`ch${item.id}`}>{item.name}</label>
</div>
))}
<div className="check-form-box">
<input type="checkbox" id="ch05" checked={isOtherChecked} />
<label htmlFor="ch05"> ()</label>
</div>
</div>
<div className="data-input">
<input type="text" className="input-frame" disabled defaultValue={''} />
</div>
</>
)
}

View File

@ -0,0 +1,128 @@
'use client'
import { useState } from 'react'
import { SurveyDetailRequest } from '@/types/Survey'
type RadioEtcKeys = 'house_structure' | 'rafter_material' | 'waterproof_material' | 'insulation_presence'
const translateJapanese: Record<RadioEtcKeys, string> = {
house_structure: '住宅構造',
rafter_material: '垂木材質',
waterproof_material: '防水材の種類',
insulation_presence: '断熱材の有無',
}
const radioEtcData: Record<RadioEtcKeys, { id: number; label: string }[]> = {
house_structure: [
{
id: 1,
label: '木製',
},
],
rafter_material: [
{
id: 1,
label: '木製',
},
{
id: 2,
label: '強制',
},
],
waterproof_material: [
{
id: 1,
label: 'アスファルト屋根94022kg以上',
},
],
insulation_presence: [
{
id: 1,
label: 'なし',
},
{
id: 2,
label: 'あり',
},
],
}
export default function RadioEtc({
column,
setDetailInfoData,
detailInfoData,
}: {
column: RadioEtcKeys
setDetailInfoData: (data: any) => void
detailInfoData: SurveyDetailRequest
}) {
const [isEtcSelected, setIsEtcSelected] = useState(false)
const [etcValue, setEtcValue] = useState('')
const handleRadioChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
if (column === 'insulation_presence') {
setIsEtcSelected(value === '2')
setDetailInfoData({
...detailInfoData,
[column]: Number(value),
})
} else if (value === 'etc') {
setIsEtcSelected(true)
setDetailInfoData({
...detailInfoData,
[column]: null,
})
} else {
setIsEtcSelected(false)
setEtcValue('')
setDetailInfoData({
...detailInfoData,
[column]: Number(value),
[`${column}_etc`]: null,
})
}
}
const handleEtcInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
setEtcValue(value)
setDetailInfoData({
...detailInfoData,
[`${column}_etc`]: value,
})
}
return (
<div className="data-input-form-bx">
<div className="data-input-form-tit red-f">{translateJapanese[column]}</div>
{radioEtcData[column].map((item) => (
<div className="radio-form-box mb10" key={item.id}>
<input
type="radio"
name={column}
id={`${column}_${item.id}`}
value={item.id}
onChange={handleRadioChange}
checked={detailInfoData[column] === item.id}
/>
<label htmlFor={`${column}_${item.id}`}>{item.label}</label>
</div>
))}
{column !== 'insulation_presence' && (
<div className="radio-form-box mb10">
<input type="radio" name={column} id={`${column}`} value="etc" onChange={handleRadioChange} />
<label htmlFor={`${column}_etc`}> ()</label>
</div>
)}
<div className="data-input">
<input
type="text"
className="input-frame"
disabled={column === 'insulation_presence' ? !isEtcSelected : !isEtcSelected}
value={etcValue}
onChange={handleEtcInputChange}
/>
</div>
</div>
)
}

View File

@ -0,0 +1,321 @@
'use client'
import { useSurveySaleTabState } from '@/store/surveySaleTabState'
import { useServey } from '@/hooks/useSurvey'
import { SurveyDetailRequest } from '@/types/Survey'
import { useRouter, useSearchParams } from 'next/navigation'
import { useEffect, useState } from 'react'
import MultiCheckEtc from './MultiCheckEtc'
import SelectBoxEtc from './SelectBoxEtc'
import RadioEtc from './RadioEtc'
const defaultDetailInfoForm: SurveyDetailRequest = {
contract_capacity: null,
retail_company: null,
supplementary_facilities: null,
supplementary_facilities_etc: null,
installation_system: null,
installation_system_etc: null,
construction_year: null,
construction_year_etc: null,
roof_material: null,
roof_material_etc: null,
roof_shape: null,
roof_shape_etc: null,
roof_slope: null,
house_structure: 1,
house_structure_etc: null,
rafter_material: 1,
rafter_material_etc: null,
rafter_size: null,
rafter_size_etc: null,
rafter_pitch: null,
rafter_pitch_etc: null,
rafter_direction: 1,
open_field_plate_kind: null,
open_field_plate_kind_etc: null,
open_field_plate_thickness: null,
leak_trace: false,
waterproof_material: null,
waterproof_material_etc: null,
insulation_presence: 1,
insulation_presence_etc: null,
structure_order: null,
structure_order_etc: null,
installation_availability: null,
installation_availability_etc: null,
memo: null,
}
export default function RoofInfoForm() {
const { setRoofInfoSelected } = useSurveySaleTabState()
useEffect(() => {
setRoofInfoSelected()
}, [])
const router = useRouter()
const searchParams = useSearchParams()
const id = searchParams.get('id')
const { surveyDetail, createSurveyDetail, validateSurveyDetail } = useServey(Number(id))
const [detailInfoData, setDetailInfoData] = useState<SurveyDetailRequest>(defaultDetailInfoForm)
useEffect(() => {
if (surveyDetail?.detail_info) {
const { id, updated_at, created_at, ...rest } = surveyDetail.detail_info
setDetailInfoData(rest)
}
}, [surveyDetail])
const handleNumberInput = (key: keyof SurveyDetailRequest, value: number | string) => {
if (typeof value === 'string') {
const numberValue = value === '' ? null : Number(value)
setDetailInfoData({ ...detailInfoData, [key]: numberValue })
} else {
setDetailInfoData({ ...detailInfoData, [key]: value })
}
}
const handleTextInput = (key: keyof SurveyDetailRequest, value: string) => {
setDetailInfoData({ ...detailInfoData, [key]: value || null })
}
const handleBooleanInput = (key: keyof SurveyDetailRequest, value: boolean) => {
setDetailInfoData({ ...detailInfoData, [key]: value })
}
const handleUnitInput = (value: string) => {
const numericValue = detailInfoData.contract_capacity?.replace(/[^0-9.]/g, '') || ''
setDetailInfoData({
...detailInfoData,
contract_capacity: numericValue ? `${numericValue} ${value}` : value,
})
}
// TODO: 조사매물 저장 요구사항 정립 이후 수정 필요
const handleSave = async () => {
if (id) {
const emptyField = validateSurveyDetail(detailInfoData)
if (emptyField.trim() === '') {
createSurveyDetail({ surveyId: Number(id), surveyDetail: detailInfoData })
router.push(`/survey-sale`)
} else {
alert(emptyField + ' is required')
focusOnInput(emptyField)
}
} else {
alert('save essential information first')
}
}
const focusOnInput = (field: string) => {
const input = document.getElementById(field)
if (input) {
input.focus()
}
}
return (
<>
<div className="sale-frame">
<div className="sale-roof-title"></div>
<div className="data-form-wrap">
<div className="data-input-form-bx">
{/* 전기계약 용량 - contract_capacity */}
<div className="data-input-form-tit"></div>
<div className="data-input mb5">
<input
type="text"
className="input-frame"
value={detailInfoData.contract_capacity?.split(' ')[0] ?? ''}
onChange={(e) => handleTextInput('contract_capacity', e.target.value)}
/>
</div>
<div className="data-input">
<select
className="select-form"
name="contract_capacity_unit"
id="contract_capacity_unit"
onChange={(e) => handleUnitInput(e.target.value)}
>
<option value="kVA">kVA</option>
<option value="A">A</option>
</select>
</div>
</div>
{/* 전기 소매 회사 - retail_company */}
<div className="data-input-form-bx">
<div className="data-input-form-tit"></div>
<input
type="text"
className="input-frame"
value={detailInfoData.retail_company ?? ''}
onChange={(e) => handleTextInput('retail_company', e.target.value)}
/>
</div>
{/* 전기 부대 설비 - supplementary_facilities */}
<div className="data-input-form-bx">
<MultiCheckEtc column={'supplementary_facilities'} setDetailInfoData={setDetailInfoData} detailInfoData={detailInfoData} />
</div>
{/* 설치 희망 시스템 - installation_system */}
<SelectBoxEtc column={'installation_system'} setDetailInfoData={setDetailInfoData} detailInfoData={detailInfoData} />
</div>
</div>
<div className="sale-frame">
<div className="sale-roof-title"></div>
<div className="data-form-wrap">
{/* 건축 연수 - construction_year */}
<SelectBoxEtc column={'construction_year'} setDetailInfoData={setDetailInfoData} detailInfoData={detailInfoData} />
{/* 지붕재 - roof_material */}
<div className="data-input-form-bx">
<MultiCheckEtc column={'roof_material'} setDetailInfoData={setDetailInfoData} detailInfoData={detailInfoData} />
</div>
{/* 지붕 모양 - roof_shape */}
<SelectBoxEtc column={'roof_shape'} setDetailInfoData={setDetailInfoData} detailInfoData={detailInfoData} />
{/* 지붕 경사도 - roof_slope */}
<div className="data-input-form-bx">
<div className="data-input-form-tit"></div>
<div className="data-input flex">
<input
type="text"
className="input-frame"
value={detailInfoData.roof_slope ?? ''}
onChange={(e) => handleTextInput('roof_slope', e.target.value)}
/>
<span></span>
</div>
</div>
{/* 주택 구조 - house_structure */}
<RadioEtc column={'house_structure'} setDetailInfoData={setDetailInfoData} detailInfoData={detailInfoData} />
{/* 서까래 재질 - rafter_material */}
<RadioEtc column={'rafter_material'} setDetailInfoData={setDetailInfoData} detailInfoData={detailInfoData} />
{/* 서까래 크기 - rafter_size */}
<SelectBoxEtc column={'rafter_size'} setDetailInfoData={setDetailInfoData} detailInfoData={detailInfoData} />
{/* 서까래 피치 - rafter_pitch */}
<SelectBoxEtc column={'rafter_pitch'} setDetailInfoData={setDetailInfoData} detailInfoData={detailInfoData} />
{/* 서까래 방향 - rafter_direction */}
<div className="data-input-form-bx">
<div className="data-input-form-tit red-f"></div>
<div className="data-check-wrap mb0" id="rafter_direction">
<div className="radio-form-box">
<input
type="radio"
name="rafter_direction"
id="rafter_direction_1"
value={1}
onChange={(e) => handleNumberInput('rafter_direction', e.target.value)}
checked={detailInfoData.rafter_direction === 1}
/>
<label htmlFor="rafter_direction_1"></label>
</div>
<div className="radio-form-box">
<input
type="radio"
name="rafter_direction"
id="rafter_direction_2"
value={2}
onChange={(e) => handleNumberInput('rafter_direction', e.target.value)}
checked={detailInfoData.rafter_direction === 2}
/>
<label htmlFor="rafter_direction_2"></label>
</div>
</div>
</div>
{/* 노지판 종류 - open_field_plate_kind */}
<SelectBoxEtc column={'open_field_plate_kind'} setDetailInfoData={setDetailInfoData} detailInfoData={detailInfoData} />
{/* 노지판 두께 - open_field_plate_thickness */}
<div className="data-input-form-bx">
<div className="data-input-form-tit">
<span>, . </span>
</div>
<div className="data-input flex">
<input
type="text"
className="input-frame"
value={detailInfoData.open_field_plate_thickness ?? ''}
onChange={(e) => handleTextInput('open_field_plate_thickness', e.target.value)}
/>
<span>mm</span>
</div>
</div>
{/* 누수 흔적 - leak_trace */}
<div className="data-input-form-bx">
<div className="data-input-form-tit "></div>
<div className="data-check-wrap mb0">
<div className="radio-form-box">
<input
type="radio"
name="leak_trace"
id="leak_trace_1"
checked={detailInfoData.leak_trace === true}
onChange={(e) => handleBooleanInput('leak_trace', true)}
/>
<label htmlFor="leak_trace_1"></label>
</div>
<div className="radio-form-box">
<input
type="radio"
name="leak_trace"
id="leak_trace_2"
checked={detailInfoData.leak_trace === false}
onChange={(e) => handleBooleanInput('leak_trace', false)}
/>
<label htmlFor="leak_trace_2"></label>
</div>
</div>
</div>
{/* 방수재 종류 - waterproof_material */}
<RadioEtc column={'waterproof_material'} setDetailInfoData={setDetailInfoData} detailInfoData={detailInfoData} />
{/* 단열재 유무 - insulation_presence */}
<RadioEtc column={'insulation_presence'} setDetailInfoData={setDetailInfoData} detailInfoData={detailInfoData} />
{/* 노지판 종류 - open_field_plate_kind */}
<SelectBoxEtc column={'structure_order'} setDetailInfoData={setDetailInfoData} detailInfoData={detailInfoData} />
{/* 설치 가능 여부 - installation_availability */}
<SelectBoxEtc column={'installation_availability'} setDetailInfoData={setDetailInfoData} detailInfoData={detailInfoData} />
{/* 메모 - memo */}
<div className="data-input-form-bx">
<div className="data-input-form-tit"></div>
<div className="data-input mb5">
<select className="select-form" name="" id="">
<option value=""></option>
<option value=""></option>
<option value=""></option>
<option value=""></option>
<option value=""></option>
</select>
</div>
<div className="data-input">
<textarea
className="textarea-form"
name="memo"
id="memo"
value={detailInfoData.memo ?? ''}
onChange={(e) => handleTextInput('memo', e.target.value)}
placeholder="例: 漏れの兆候があるため、正確な点検が必要です."
></textarea>
</div>
</div>
</div>
<div className="btn-flex-wrap">
<div className="btn-bx">
<button className="btn-frame n-blue icon" onClick={handleSave}>
<i className="btn-arr"></i>
</button>
</div>
<div className="btn-bx">
<button className="btn-frame red icon" onClick={handleSave}>
<i className="btn-arr"></i>
</button>
</div>
<div className="btn-bx">
<button className="btn-frame n-blue icon" onClick={() => router.push('/survey-sale')}>
<i className="btn-arr"></i>
</button>
</div>
</div>
</div>
</>
)
}

View File

@ -0,0 +1,234 @@
import type { SurveyDetailRequest } from '@/types/Survey'
import { useEffect, useState } from 'react'
type SelectBoxKeys =
| 'installation_system'
| 'construction_year'
| 'roof_shape'
| 'rafter_pitch'
| 'rafter_size'
| 'open_field_plate_kind'
| 'structure_order'
| 'installation_availability'
const font: Record<SelectBoxKeys, string> = {
installation_system: 'data-input-form-tit red-f',
construction_year: 'data-input-form-tit red-f',
roof_shape: 'data-input-form-tit',
rafter_pitch: 'data-input-form-tit red-f',
rafter_size: 'data-input-form-tit red-f',
open_field_plate_kind: 'data-input-form-tit',
structure_order: 'data-input-form-tit red-f',
installation_availability: 'data-input-form-tit',
}
const translateJapanese: Record<SelectBoxKeys, string> = {
installation_system: '設置希望システム',
construction_year: '建築年数',
roof_shape: '屋根の形状',
rafter_pitch: '垂木傾斜',
rafter_size: '垂木サイズ',
open_field_plate_kind: '路地板の種類',
structure_order: '屋根構造の順序',
installation_availability: '屋根製品名 設置可否確認',
}
const selectBoxOptions: Record<SelectBoxKeys, { id: number; name: string }[]> = {
installation_system: [
{
id: 1,
name: '太陽光発電', //태양광발전
},
{
id: 2,
name: 'ハイブリッド蓄電システム', //하이브리드축전지시스템
},
{
id: 3,
name: '蓄電池システム', //축전지시스템
},
],
construction_year: [
{
id: 1,
name: '新築', //신축
},
{
id: 2,
name: '既築', //기존
},
],
roof_shape: [
{
id: 1,
name: '切妻', //박공지붕
},
{
id: 2,
name: '寄棟', //기동
},
{
id: 3,
name: '片流れ', //한쪽흐름
},
],
rafter_size: [
{
id: 1,
name: '幅35mm以上×高さ48mm以上',
},
{
id: 2,
name: '幅36mm以上×高さ46mm以上',
},
{
id: 3,
name: '幅37mm以上×高さ43mm以上',
},
{
id: 4,
name: '幅38mm以上×高さ40mm以上',
},
],
rafter_pitch: [
{
id: 1,
name: '(455mm以下',
},
{
id: 2,
name: '500mm以下',
},
{
id: 3,
name: '606mm以下',
},
],
open_field_plate_kind: [
{
id: 1,
name: '構造用合板', //구조용합판
},
{
id: 2,
name: 'OSB', //OSB
},
{
id: 3,
name: 'パーティクルボード', //파티클보드
},
{
id: 4,
name: '小幅板', //소판
},
],
structure_order: [
{
id: 1,
name: '屋根材', //지붕재
},
{
id: 2,
name: '防水材', //방수재
},
{
id: 3,
name: '屋根の基礎', //지붕의기초
},
{
id: 4,
name: '垂木', //서까래
},
],
installation_availability: [
{
id: 1,
name: '確認済み', //확인완료
},
{
id: 2,
name: '未確認', //미확인
},
],
}
export default function SelectBoxForm({
column,
setDetailInfoData,
detailInfoData,
}: {
column: SelectBoxKeys
setDetailInfoData: (data: any) => void
detailInfoData: SurveyDetailRequest
}) {
const [isEtcSelected, setIsEtcSelected] = useState(false)
const [etcValue, setEtcValue] = useState('')
useEffect(() => {
setEtcValue(detailInfoData[`${column}_etc`] ?? '')
}, [detailInfoData[`${column}_etc`]])
const handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value
if (column === 'installation_availability' || column === 'construction_year') {
setIsEtcSelected(value === '2') // 既築(2) 또는 未確認(2) 선택 시 input 활성화
setDetailInfoData({
...detailInfoData,
[column]: Number(value),
})
} else if (value === 'etc') {
setIsEtcSelected(true)
setDetailInfoData({
...detailInfoData,
[column]: null,
})
} else {
setIsEtcSelected(false)
setEtcValue('')
setDetailInfoData({
...detailInfoData,
[`${column}_etc`]: null,
[column]: Number(value),
})
}
}
const handleEtcInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
setEtcValue(value)
setDetailInfoData({
...detailInfoData,
[`${column}_etc`]: value,
})
}
return (
<>
<div className="data-input-form-bx">
<div className={font[column]}>{translateJapanese[column]}</div>
<div className="data-input mb5">
<select className="select-form" name={column} id={column} onChange={handleSelectChange} value={detailInfoData[column] ?? ''}>
<option value="" hidden>
</option>
{selectBoxOptions[column].map((option) => (
<option key={option.id} value={option.id}>
{option.name}
</option>
))}
{column !== 'installation_availability' && column !== 'construction_year' && <option value="etc"> ()</option>}
</select>
</div>
<div className="data-input">
<input
type="text"
className="input-frame"
value={etcValue ?? ''}
onChange={handleEtcInputChange}
disabled={column === 'installation_availability' || column === 'construction_year' ? !Boolean(detailInfoData[column]) : !isEtcSelected}
/>
</div>
</div>
</>
)
}

View File

@ -2,39 +2,72 @@
import LoadMoreButton from '@/components/LoadMoreButton'
import { useServey } from '@/hooks/useSurvey'
import { useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { useRouter } from 'next/navigation'
import SearchForm from './SearchForm'
export default function ListTable() {
const router = useRouter()
const { surveyList, isLoadingSurveyList } = useServey()
const [hasMore, setHasMore] = useState(surveyList.length > 5)
const [visibleItems, setVisibleItems] = useState(5)
const [search, setSearch] = useState('')
const [isMyPostsOnly, setIsMyPostsOnly] = useState(false)
const [hasMore, setHasMore] = useState(false)
// TODO: 로그인 구현 이후 USERNAME 변경
const username = 'test'
const filteredSurveyList = useMemo(() => {
let filtered = surveyList
if (search.trim().length > 0) {
filtered = filtered.filter((survey) => survey.building_name?.includes(search))
}
if (isMyPostsOnly) {
filtered = filtered.filter((survey) => survey.representative === username)
}
return filtered
}, [surveyList, search, isMyPostsOnly, username])
useEffect(() => {
setHasMore(filteredSurveyList.length > visibleItems)
}, [filteredSurveyList, visibleItems])
const handleLoadMore = () => {
const newVisibleItems = Math.min(visibleItems + 5, surveyList.length)
const newVisibleItems = Math.min(visibleItems + 5, filteredSurveyList.length)
setVisibleItems(newVisibleItems)
setHasMore(newVisibleItems < surveyList.length)
setHasMore(newVisibleItems < filteredSurveyList.length)
}
const handleScrollToTop = () => {
window.scrollTo({ top: 0, behavior: 'smooth' })
}
const handleDetail = (id: number) => {
const handleDetailClick = (id: number) => {
router.push(`/survey-sale/${id}`)
}
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value)
setVisibleItems(5)
}
const handleMyPostsToggle = () => {
setIsMyPostsOnly((prev) => !prev)
setVisibleItems(5)
}
if (isLoadingSurveyList) {
return <div>Loading...</div>
}
return (
<>
<SearchForm handleSearch={handleSearchChange} handleMyPosts={handleMyPostsToggle} />
<div className="sale-frame">
<ul className="sale-list-wrap">
{surveyList.slice(0, visibleItems).map((survey) => (
<li className="sale-list-item cursor-pointer" key={survey.id} onClick={() => handleDetail(survey.id)}>
{filteredSurveyList.slice(0, visibleItems).map((survey) => (
<li className="sale-list-item cursor-pointer" key={survey.id} onClick={() => handleDetailClick(survey.id)}>
<div className="sale-item-bx">
<div className="sale-item-date-bx">
<div className="sale-item-num">{survey.id}</div>

View File

@ -2,7 +2,7 @@
import { useRouter } from 'next/navigation'
export default function SearchForm() {
export default function SearchForm({ handleSearch, handleMyPosts }: { handleSearch: (e: React.ChangeEvent<HTMLInputElement>) => void, handleMyPosts: () => void }) {
const router = useRouter()
return (
<div className="sale-frame">
@ -22,13 +22,13 @@ export default function SearchForm() {
</div>
<div className="sale-form-bx">
<div className="search-input">
<input type="text" className="search-frame" placeholder="タイトルを入力してください. (2文字以上)" />
<input type="text" className="search-frame" placeholder="タイトルを入力してください. (2文字以上)" onChange={handleSearch} />
<button className="search-icon"></button>
</div>
</div>
<div className="sale-form-bx">
<div className="check-form-box">
<input type="checkbox" id="ch01" />
<input type="checkbox" id="ch01" onClick={handleMyPosts} />
<label htmlFor="ch01"></label>
</div>
</div>

View File

@ -1,68 +0,0 @@
'use client'
import { useState } from "react"
import { SurveyDetailRequest } from "@/types/Survey"
interface EtcCheckboxProps {
formName: keyof SurveyDetailRequest
label: string
detailInfoForm: SurveyDetailRequest
setDetailInfoForm: (form: SurveyDetailRequest) => void
}
export default function EtcCheckbox({ formName, label, detailInfoForm, setDetailInfoForm }: EtcCheckboxProps) {
const [showEtcInput, setShowEtcInput] = useState(false)
const etcFieldName = `${formName}_etc` as keyof SurveyDetailRequest
return (
<div className="space-y-2">
<label htmlFor={formName} className="block font-medium">{label}</label>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<input
type="checkbox"
id={formName}
checked={detailInfoForm[formName] === 1}
onChange={(e) => setDetailInfoForm({
...detailInfoForm,
[formName]: e.target.checked ? 1 : 0
})}
/>
<label htmlFor={formName}></label>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
id={`${formName}_etc_check`}
checked={showEtcInput}
onChange={(e) => {
setShowEtcInput(e.target.checked)
if (!e.target.checked) {
setDetailInfoForm({
...detailInfoForm,
[etcFieldName]: ''
})
}
}}
/>
<label htmlFor={`${formName}_etc_check`}></label>
{showEtcInput && (
<input
type="text"
id={`${formName}_etc`}
value={(detailInfoForm[etcFieldName] as string | null) ?? ''}
placeholder="기타 사항을 입력하세요"
onChange={(e) => setDetailInfoForm({
...detailInfoForm,
[etcFieldName]: e.target.value
})}
className="border rounded px-2 py-1 ml-2"
/>
)}
</div>
</div>
</div>
)
}

View File

@ -1,61 +0,0 @@
// 'use client'
// import { useServey } from '@/hooks/useSurvey'
// import { useParams, useRouter } from 'next/navigation'
// export default function SurveyDetail() {
// const params = useParams()
// const id = params.id
// const router = useRouter()
// const { surveyDetail, deleteSurvey, isDeletingSurvey, confirmSurvey } = useServey(Number(id))
// console.log('surveyDetail:: ', surveyDetail)
// const handleDelete = async () => {
// if (confirm('delete?')) {
// if (surveyDetail?.representative) {
// if (surveyDetail.detail_info?.id) {
// await deleteSurvey({ id: Number(surveyDetail.detail_info.id), isDetail: true })
// }
// await deleteSurvey({ id: Number(id), isDetail: false })
// }
// alert('delete success')
// router.push('/survey-sales')
// }
// }
// const handleSubmit = () => {
// if (confirm('submit?')) {
// confirmSurvey(Number(id))
// }
// alert('submit success')
// router.push('/survey-sales')
// }
// if (isDeletingSurvey) {
// return <div>Deleting...</div>
// }
// return (
// <div>
// <h1>SurveyDetail</h1>
// <p>{id}</p>
// <p>{surveyDetail?.representative}</p>
// <div className="flex gap-3">
// <button className="bg-blue-500 text-white px-2 py-1 rounded-md cursor-pointer" onClick={() => router.push('/survey-sales/write?id=' + id)}>
// edit
// </button>
// <button className="bg-blue-500 text-white px-2 py-1 rounded-md cursor-pointer" onClick={handleSubmit}>
// submit
// </button>
// <button className="bg-red-500 text-white px-2 py-1 rounded-md cursor-pointer" onClick={handleDelete}>
// delete
// </button>
// <button className="bg-gray-500 text-white px-2 py-1 rounded-md cursor-pointer" onClick={() => router.back()}>
// back
// </button>
// </div>
// <input type="text" className="input-frame" disabled defaultValue={surveyDetail?.store ?? ''} />
// </div>
// )
// }

View File

@ -1,22 +0,0 @@
import { Search } from 'lucide-react'
import { useRouter } from 'next/navigation'
export default function SurveyFilter({ handleSearch, handleMyPosts }: { handleSearch: (e: React.ChangeEvent<HTMLInputElement>) => void, handleMyPosts: () => void }) {
const router = useRouter()
return (
<div>
<button onClick={() => router.push('/survey-sales/write')}>write survey {'>'}</button>
<div className="flex items-center gap-2">
<input type="text" placeholder="Search" onChange={handleSearch} />
<button>
<Search />
</button>
</div>
<div className="flex items-center gap-2">
<button onClick={handleMyPosts} className="cursor-pointer">
my posts
</button>
</div>
</div>
)
}

View File

@ -1,90 +0,0 @@
'use client'
import { useServey } from '@/hooks/useSurvey'
import LoadMoreButton from '@/components/LoadMoreButton'
import { useState } from 'react'
import SurveyFilter from './SurveyFilter'
import { useRouter } from 'next/navigation'
export default function SurveySaleList() {
const { surveyList, isLoadingSurveyList } = useServey()
const [search, setSearch] = useState('')
const [isMyPostsOnly, setIsMyPostsOnly] = useState(false)
const [hasMore, setHasMore] = useState(surveyList.length > 5)
const [visibleItems, setVisibleItems] = useState(5)
const router = useRouter()
// TEMP USERNAME
const username = 'test'
const surveyData = () => {
if (search.trim().length > 0) {
return surveyList.filter((survey) => survey.building_name?.includes(search))
}
if (isMyPostsOnly) {
return surveyList.filter((survey) => survey.representative === username)
}
return surveyList
}
const handleLoadMore = () => {
const newVisibleItems = Math.min(visibleItems + 5, surveyData().length)
setVisibleItems(newVisibleItems)
setHasMore(newVisibleItems < surveyData().length)
}
const handleScrollToTop = () => {
window.scrollTo({ top: 0, behavior: 'smooth' })
}
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value)
}
const handleDetail = (id: number | undefined) => {
if (id === undefined) throw new Error('id is required')
router.push(`/survey-sales/${id}`)
}
if (isLoadingSurveyList) {
return <div>Loading...</div>
}
return (
<div className="max-w-4xl mx-auto p-4">
<SurveyFilter handleSearch={handleSearch} handleMyPosts={() => setIsMyPostsOnly(!isMyPostsOnly)} />
<div className="grid gap-4 mt-6">
{surveyData().slice(0, visibleItems).map((survey) => (
<div
key={survey.id}
onClick={() => handleDetail(survey.id)}
className="bg-white rounded-lg shadow p-4 hover:shadow-md transition-shadow cursor-pointer border border-gray-200"
>
<div className="flex justify-between items-start">
<div className="space-y-2">
<h2 className="text-lg font-semibold text-gray-900">{survey.id}</h2>
<div className="space-y-1 text-sm text-gray-600">
<p>: {survey.representative || '-'}</p>
<p>: {survey.store || '-'}</p>
</div>
</div>
<span
className={`px-3 py-1 rounded-full text-sm ${
survey.submission_status ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'
}`}
>
{survey.submission_status ? '제출' : '미제출'}
</span>
</div>
</div>
))}
</div>
<div className="mt-6">
<LoadMoreButton hasMore={hasMore} onLoadMore={handleLoadMore} onScrollToTop={handleScrollToTop} />
</div>
</div>
)
}

View File

@ -1,86 +0,0 @@
'use client'
import { SurveyBasicRequest } from '@/types/Survey'
export default function BasicWriteForm({
basicInfoData,
setBasicInfoData,
}: {
basicInfoData: SurveyBasicRequest
setBasicInfoData: (basicInfoData: SurveyBasicRequest) => void
}) {
const handleChange = (key: keyof SurveyBasicRequest, value: string) => {
setBasicInfoData({ ...basicInfoData, [key]: value.toString() })
}
return (
<div>
<div>
<label htmlFor="representative"></label>
<input
type="text"
id="representative"
value={basicInfoData.representative}
onChange={(e) => handleChange('representative', e.target.value)}
/>
</div>
<div>
<label htmlFor="store"></label>
<input type="text" id="store" value={basicInfoData.store ?? ''} onChange={(e) => handleChange('store', e.target.value)} />
</div>
<div>
<label htmlFor="construction_point"></label>
<input
type="text"
id="construction_point"
value={basicInfoData.construction_point ?? ''}
onChange={(e) => handleChange('construction_point', e.target.value)}
/>
</div>
<div>
<label htmlFor="investigation_date"> </label>
<input
type="date"
id="investigation_date"
value={basicInfoData.investigation_date ?? ''}
onChange={(e) => handleChange('investigation_date', e.target.value)}
/>
</div>
<div>
<label htmlFor="building_name"></label>
<input
type="text"
id="building_name"
value={basicInfoData.building_name ?? ''}
onChange={(e) => handleChange('building_name', e.target.value)}
/>
</div>
<div>
<label htmlFor="customer_name"></label>
<input
type="text"
id="customer_name"
value={basicInfoData.customer_name ?? ''}
onChange={(e) => handleChange('customer_name', e.target.value)}
/>
</div>
<div>
<label htmlFor="post_code"></label>
<input type="text" id="post_code" value={basicInfoData.post_code ?? ''} onChange={(e) => handleChange('post_code', e.target.value)} />
</div>
<div>
<label htmlFor="address"></label>
<input type="text" id="address" value={basicInfoData.address ?? ''} onChange={(e) => handleChange('address', e.target.value)} />
</div>
<div>
<label htmlFor="address_detail"></label>
<input
type="text"
id="address_detail"
value={basicInfoData.address_detail ?? ''}
onChange={(e) => handleChange('address_detail', e.target.value)}
/>
</div>
</div>
)
}

View File

@ -1,157 +0,0 @@
'use client'
import React from 'react'
import EtcCheckbox from '../EtcCheckbox'
import { SurveyDetailRequest } from '@/types/Survey'
interface DetailWriteFormProps {
detailInfoForm: SurveyDetailRequest
setDetailInfoForm: (form: SurveyDetailRequest) => void
}
export default function DetailWriteForm({ detailInfoForm, setDetailInfoForm }: DetailWriteFormProps) {
const handleNumberInput = (field: keyof SurveyDetailRequest, value: string) => {
const numberValue = value === '' ? null : Number(value)
setDetailInfoForm({ ...detailInfoForm, [field]: numberValue })
}
const handleTextInput = (field: keyof SurveyDetailRequest, value: string) => {
setDetailInfoForm({ ...detailInfoForm, [field]: value || null })
}
const handleBooleanInput = (field: keyof SurveyDetailRequest, checked: boolean) => {
setDetailInfoForm({ ...detailInfoForm, [field]: checked })
}
return (
<div className="space-y-4">
<div>
<label htmlFor="contract_capacity"></label>
<input
type="text"
id="contract_capacity"
value={detailInfoForm.contract_capacity ?? ''}
onChange={(e) => handleTextInput('contract_capacity', e.target.value)}
/>
</div>
<div>
<label htmlFor="retail_company"></label>
<input
type="text"
id="retail_company"
value={detailInfoForm.retail_company ?? ''}
onChange={(e) => handleTextInput('retail_company', e.target.value)}
/>
</div>
<EtcCheckbox
formName="supplementary_facilities"
label="부대설비"
detailInfoForm={detailInfoForm}
setDetailInfoForm={setDetailInfoForm}
/>
<div>
<EtcCheckbox formName="installation_system" label="설치시스템" detailInfoForm={detailInfoForm} setDetailInfoForm={setDetailInfoForm} />
</div>
<div>
<EtcCheckbox formName="construction_year" label="건축년도" detailInfoForm={detailInfoForm} setDetailInfoForm={setDetailInfoForm} />
</div>
<div>
<EtcCheckbox formName="roof_material" label="지붕재료" detailInfoForm={detailInfoForm} setDetailInfoForm={setDetailInfoForm} />
</div>
<div>
<EtcCheckbox formName="roof_shape" label="지붕형태" detailInfoForm={detailInfoForm} setDetailInfoForm={setDetailInfoForm} />
</div>
<div>
<label htmlFor="roof_slope"> </label>
<input
type="text"
id="roof_slope"
value={detailInfoForm.roof_slope ?? ''}
onChange={(e) => handleTextInput('roof_slope', e.target.value)}
/>
</div>
<div>
<EtcCheckbox formName="house_structure" label="주택 구조" detailInfoForm={detailInfoForm} setDetailInfoForm={setDetailInfoForm} />
</div>
<div>
<EtcCheckbox formName="rafter_material" label="주탑재료" detailInfoForm={detailInfoForm} setDetailInfoForm={setDetailInfoForm} />
</div>
<div>
<EtcCheckbox formName="rafter_size" label="주탑 크기" detailInfoForm={detailInfoForm} setDetailInfoForm={setDetailInfoForm} />
</div>
<div>
<EtcCheckbox formName="rafter_pitch" label="주탑 경사도" detailInfoForm={detailInfoForm} setDetailInfoForm={setDetailInfoForm} />
</div>
<div>
<label htmlFor="rafter_direction"> </label>
<input
type="text"
id="rafter_direction"
value={detailInfoForm.rafter_direction ?? ''}
onChange={(e) => handleTextInput('rafter_direction', e.target.value)}
/>
</div>
<div>
<EtcCheckbox formName="open_field_plate_kind" label="노지판 형태" detailInfoForm={detailInfoForm} setDetailInfoForm={setDetailInfoForm} />
</div>
<div>
<label htmlFor="open_field_plate_thickness"> </label>
<input
type="text"
id="open_field_plate_thickness"
value={detailInfoForm.open_field_plate_thickness ?? ''}
onChange={(e) => handleTextInput('open_field_plate_thickness', e.target.value)}
/>
</div>
<div>
<label htmlFor="leak_trace"> </label>
<input
type="checkbox"
id="leak_trace"
checked={detailInfoForm.leak_trace ?? false}
onChange={(e) => handleBooleanInput('leak_trace', e.target.checked)}
/>
</div>
<div>
<EtcCheckbox formName="waterproof_material" label="방수재료" detailInfoForm={detailInfoForm} setDetailInfoForm={setDetailInfoForm} />
</div>
<div>
<EtcCheckbox formName="insulation_presence" label="보증금 존재" detailInfoForm={detailInfoForm} setDetailInfoForm={setDetailInfoForm} />
</div>
<div>
<EtcCheckbox formName="structure_order" label="구조체계" detailInfoForm={detailInfoForm} setDetailInfoForm={setDetailInfoForm} />
</div>
<div>
<EtcCheckbox formName="installation_availability" label="설치가능" detailInfoForm={detailInfoForm} setDetailInfoForm={setDetailInfoForm} />
</div>
<div>
<label htmlFor="memo"></label>
<textarea
id="memo"
value={detailInfoForm.memo ?? ''}
onChange={(e) => handleTextInput('memo', e.target.value)}
className="w-full"
/>
</div>
</div>
)
}

View File

@ -1,168 +0,0 @@
// 'use client'
// import { useState, useEffect } from 'react'
// import BasicWriteForm from './BasicWriteForm'
// import DetailWriteForm from './DetailWriteForm'
// import { SurveySalesBasicInfo, SurveySalesDetailInfo } from '@/api/surveySales'
// import { useRouter, useSearchParams } from 'next/navigation'
// import { useServey } from '@/hooks/useSurvey'
// import { SurveyBasicRequest, SurveyDetailRequest } from '@/types/Survey'
// type TabType = 'basic' | 'detail'
// const defaultDetailInfoForm: SurveyDetailRequest = {
// basic_info_id: 0,
// contract_capacity: null,
// retail_company: null,
// supplementary_facilities: null,
// supplementary_facilities_etc: null,
// installation_system: null,
// installation_system_etc: null,
// construction_year: null,
// construction_year_etc: null,
// roof_material: null,
// roof_material_etc: null,
// roof_shape: null,
// roof_shape_etc: null,
// roof_slope: null,
// house_structure: null,
// house_structure_etc: null,
// rafter_material: null,
// rafter_material_etc: null,
// rafter_size: null,
// rafter_size_etc: null,
// rafter_pitch: null,
// rafter_pitch_etc: null,
// rafter_direction: null,
// open_field_plate_kind: null,
// open_field_plate_kind_etc: null,
// open_field_plate_thickness: null,
// leak_trace: false,
// waterproof_material: null,
// waterproof_material_etc: null,
// structure_order: null,
// structure_order_etc: null,
// insulation_presence: null,
// insulation_presence_etc: null,
// installation_availability: null,
// installation_availability_etc: null,
// memo: null,
// }
// const defaultBasicInfoForm: SurveyBasicRequest = {
// representative: '',
// store: null,
// construction_point: null,
// investigation_date: null,
// building_name: null,
// customer_name: null,
// post_code: null,
// address: null,
// address_detail: null,
// submission_status: false,
// submission_date: null,
// }
// export default function MainSurveyForm() {
// const searchParams = useSearchParams()
// const id = searchParams.get('id')
// const [activeTab, setActiveTab] = useState<TabType>('basic')
// const handleTabClick = (tab: TabType) => {
// setActiveTab(tab)
// }
// const router = useRouter()
// const { createSurvey, isCreatingSurvey, createSurveyDetail, surveyDetail, updateSurvey } = useServey(Number(id))
// const [detailInfoForm, setDetailInfoForm] = useState<SurveyDetailRequest>(defaultDetailInfoForm)
// const [basicInfoData, setBasicInfoData] = useState<SurveyBasicRequest>(defaultBasicInfoForm)
// useEffect(() => {
// if (surveyDetail) {
// setBasicInfoData({
// ...defaultBasicInfoForm,
// ...(({ id, ...rest }) => rest)(surveyDetail),
// })
// setDetailInfoForm({
// ...defaultDetailInfoForm,
// ...(surveyDetail.detail_info ? (({ id, basic_info_id, updated_at, ...rest }) => rest)(surveyDetail.detail_info as any) : {}),
// })
// }
// }, [surveyDetail])
// const handleSave = async (isSubmit: boolean = false) => {
// if (id) {
// updateSurvey({
// ...basicInfoData,
// submission_status: isSubmit,
// submission_date: isSubmit ? new Date().toISOString() : null,
// })
// router.push('/survey-sales')
// return
// }
// const surveyId = await createSurvey(basicInfoData)
// if (surveyId && surveyId !== 0) {
// createSurveyDetail({
// surveyId,
// surveyDetail: detailInfoForm,
// })
// router.push('/survey-sales')
// return
// }
// throw new Error('‼Survey creation failed')
// }
// if (isCreatingSurvey) {
// return <div>Loading...</div>
// }
// return (
// <div>
// {/* TAB BUTTONS */}
// <div>
// <button
// onClick={() => handleTabClick('basic')}
// className={`flex-1 px-4 py-2 text-center focus:outline-none focus:ring-2 focus:ring-blue-500
// ${activeTab === 'basic' ? 'border-b-2 border-blue-500 font-semibold text-blue-600' : 'text-gray-500 hover:text-gray-700'}`}
// aria-selected={activeTab === 'basic'}
// role="tab"
// >
// Basic Info
// </button>
// <button
// onClick={() => handleTabClick('detail')}
// className={`flex-1 px-4 py-2 text-center focus:outline-none focus:ring-2 focus:ring-blue-500
// ${activeTab === 'detail' ? 'border-b-2 border-blue-500 font-semibold text-blue-600' : 'text-gray-500 hover:text-gray-700'}`}
// aria-selected={activeTab === 'detail'}
// role="tab"
// >
// Detail Info
// </button>
// </div>
// {/* Tab Content */}
// <div className="mt-6">
// {activeTab === 'basic' && (
// <div className="rounded-lg border p-4">
// <h2 className="text-lg font-semibold">Basic Information</h2>
// <BasicWriteForm basicInfoData={basicInfoData} setBasicInfoData={setBasicInfoData} />
// </div>
// )}
// {activeTab === 'detail' && (
// <div className="rounded-lg border p-4">
// <h2 className="text-lg font-semibold">Detail Information</h2>
// <DetailWriteForm detailInfoForm={detailInfoForm} setDetailInfoForm={setDetailInfoForm} />
// </div>
// )}
// </div>
// <div className="flex justify-start gap-4">
// <button onClick={() => handleSave(false)}>save</button>
// <button onClick={() => handleSave(true)}>submit</button>
// <button onClick={() => router.push('/survey-sales')}>cancel</button>
// </div>
// </div>
// )
// }

View File

@ -1,16 +1,27 @@
'use client'
import { useHeaderStore } from '@/store/header'
import { useRouter } from 'next/navigation'
import { useSideNavState } from '@/store/sideNavState'
import { usePathname, useRouter } from 'next/navigation'
import { useEffect } from 'react'
export default function Main() {
const router = useRouter()
const pathname = usePathname()
const { setBackBtn } = useHeaderStore()
const { reset } = useSideNavState()
/**
*
*
*/
useEffect(() => {
setBackBtn(false)
}, [])
if (pathname === '/') {
setBackBtn(false)
}
//사이드바 초기화
reset()
}, [pathname])
return (
<>

View File

@ -1,16 +1,17 @@
'use client'
import { useEffect, useState } from 'react'
import Link from 'next/link'
import { usePathname, useRouter } from 'next/navigation'
import { Swiper, SwiperSlide } from 'swiper/react'
import { useSideNavState } from '@/store/sideNavState'
import { useHeaderStore } from '@/store/header'
import type { HeaderProps } from '@/types/Header'
import 'swiper/css'
import { useSideNavState } from '@/store/sideNavState'
import { axiosInstance } from '@/libs/axios'
// type HeaderProps = {
// name: string //header 이름
@ -20,27 +21,26 @@ import { useSideNavState } from '@/store/sideNavState'
export default function Header({ name }: HeaderProps) {
const router = useRouter()
const pathname = usePathname()
const { sideNavIsOpen, setSideNavIsOpen, reset } = useSideNavState()
const [isShowBackBtn, setIsShowBackBtn] = useState<boolean>(false)
const { sideNavIsOpen, setSideNavIsOpen } = useSideNavState()
const { backBtn } = useHeaderStore()
if (pathname === '/login') {
return null
}
useEffect(() => {
if (pathname !== '/') {
setIsShowBackBtn(true)
const handleLogout = async () => {
const { data } = await axiosInstance(null).get('/api/auth/logout')
if (data.code === 200) {
router.push('/login')
}
//사이드바 초기화
reset()
}, [pathname])
}
return (
<>
<div className="header-warp">
<header>
<div className="header-inner">
{isShowBackBtn && (
{backBtn && (
<div className="back-button-wrap">
<button className="back-button" onClick={() => router.back()}></button>
</div>
@ -112,7 +112,9 @@ export default function Header({ name }: HeaderProps) {
<div className="side-nav-footer">
<ul className="side-footer-list">
<li className="side-footer-item">
<button className="bold">LOGOUT</button>
<button className="bold" onClick={handleLogout}>
LOGOUT
</button>
</li>
<li className="side-footer-item">
<button>Jynoadmin</button>

View File

@ -15,6 +15,7 @@ export function useServey(id?: number): {
updateSurvey: (survey: SurveyBasicRequest) => void
deleteSurvey: () => Promise<boolean>
submitSurvey: () => void
validateSurveyDetail: (surveyDetail: SurveyDetailRequest) => string
} {
const queryClient = useQueryClient()
@ -97,6 +98,38 @@ export function useServey(id?: number): {
},
})
const validateSurveyDetail = (surveyDetail: SurveyDetailRequest) => {
const requiredFields = [
'installation_system',
'construction_year',
'rafter_size',
'rafter_pitch',
'rafter_direction',
'waterproof_material',
'insulation_presence',
'structure_order',
] as const;
const etcFields = [
'installation_system',
'construction_year',
'rafter_size',
'rafter_pitch',
'waterproof_material',
'structure_order',
] as const;
const emptyField = requiredFields.find((field) => {
if (etcFields.includes(field as (typeof etcFields)[number])) {
return surveyDetail[field as keyof SurveyDetailRequest] === null &&
surveyDetail[`${field}_etc` as keyof SurveyDetailRequest] === null;
}
return surveyDetail[field as keyof SurveyDetailRequest] === null;
});
return emptyField || '';
};
return {
surveyList: surveyList || [],
surveyDetail: surveyDetail || null,
@ -110,5 +143,6 @@ export function useServey(id?: number): {
deleteSurvey,
createSurveyDetail,
submitSurvey,
validateSurveyDetail,
}
}

View File

@ -1,16 +1,18 @@
import axios from 'axios'
const baseURL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'
export const axiosInstance = (url: string | null | undefined) => {
const baseURL = url || process.env.NEXT_PUBLIC_API_URL
export const axiosInstance = axios.create({
baseURL,
headers: {
'Content-Type': 'application/json',
},
})
return axios.create({
baseURL,
headers: {
Accept: 'application/json',
},
})
}
// Request interceptor
axiosInstance.interceptors.request.use(
axios.interceptors.request.use(
(config) => {
// 여기에 토큰 추가 등의 공통 로직을 넣을 수 있습니다
return config
@ -21,10 +23,44 @@ axiosInstance.interceptors.request.use(
)
// Response interceptor
axiosInstance.interceptors.response.use(
(response) => response,
axios.interceptors.response.use(
(response) => transferResponse(response),
(error) => {
// 에러 처리 로직
return Promise.reject(error)
},
)
// response데이터가 array, object에 따라 분기하여 키 변환
const transferResponse = (response: any) => {
if (!response.data) return response.data
// 배열인 경우 각 객체의 키를 변환
if (Array.isArray(response.data)) {
return response.data.map((item: any) => transformObjectKeys(item))
}
// 단일 객체인 경우
return transformObjectKeys(response.data)
}
// camel case object 반환
const transformObjectKeys = (obj: any): any => {
if (Array.isArray(obj)) {
return obj.map(transformObjectKeys)
}
if (obj !== null && typeof obj === 'object') {
return Object.keys(obj).reduce((acc: any, key: string) => {
const camelKey = snakeToCamel(key)
acc[camelKey] = transformObjectKeys(obj[key])
return acc
}, {})
}
return obj
}
// snake case to camel case
const snakeToCamel = (str: string): string => {
return str.replace(/([-_][a-z])/g, (group) => group.toUpperCase().replace('-', '').replace('_', ''))
}

View File

@ -1,5 +1,7 @@
import type { SessionData } from '@/types/Auth'
import { getIronSession, SessionOptions } from 'iron-session'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
export const sessionOptions: SessionOptions = {
cookieName: 'onsitesurvey_session',
@ -14,15 +16,34 @@ export const sessionOptions: SessionOptions = {
},
}
export interface SessionData {
username: string | null
email: string | null
isLoggedIn: boolean
}
export const defaultSession: SessionData = {
username: '',
email: '',
langCd: null,
currPage: 0,
rowCount: 0,
startRow: 0,
endRow: 0,
compCd: null,
agencyStoreId: null,
storeId: null,
userId: null,
category: null,
userNm: null,
userNmKana: null,
telNo: null,
fax: null,
email: null,
lastEditUser: null,
storeGubun: null,
pwCurr: null,
pwdInitYn: null,
apprStatCd: null,
loginFailCnt: null,
loginFailMinYn: null,
priceViewStatCd: null,
groupId: null,
storeLvl: null,
custCd: null,
builderNo: null,
isLoggedIn: false,
}
@ -31,3 +52,11 @@ export const getSession = async () => {
const session = await getIronSession<SessionData>(cookieStore, sessionOptions)
return session
}
export const logout = async () => {
const cookieStore = await cookies()
const session = await getIronSession<SessionData>(cookieStore, sessionOptions)
session.destroy()
return redirect('/login')
}

View File

@ -2,7 +2,8 @@ import { NextResponse } from 'next/server'
import { cookies } from 'next/headers'
import type { NextRequest } from 'next/server'
import { getIronSession } from 'iron-session'
import { SessionData, sessionOptions } from './libs/session'
import { sessionOptions } from './libs/session'
import type { SessionData } from './types/Auth'
export async function middleware(request: NextRequest) {
const cookieStore = await cookies()

View File

@ -0,0 +1,27 @@
import { create } from 'zustand'
type SurveyFilterState = {
keyword: string;
searchOption: string;
isMySurvey: boolean;
sort: 'recent' | 'updated';
setKeyword: (keyword: string) => void;
setSearchOption: (searchOption: string) => void;
setIsMySurvey: (isMySurvey: boolean) => void;
setSort: (sort: 'recent' | 'updated') => void;
reset: () => void;
}
export const useSurveyFilterStore = create<SurveyFilterState>((set) => ({
keyword: '',
searchOption: '',
isMySurvey: false,
sort: 'recent',
setKeyword: (keyword: string) => set({ keyword }),
setSearchOption: (searchOption: string) => set({ searchOption }),
setIsMySurvey: (isMySurvey: boolean) => set({ isMySurvey }),
setSort: (sort: 'recent' | 'updated') => set({ sort }),
reset: () => set({ keyword: '', searchOption: '', isMySurvey: false, sort: 'recent' }),
}))

30
src/types/Auth.ts Normal file
View File

@ -0,0 +1,30 @@
export interface SessionData {
langCd: null
currPage: Number | null
rowCount: Number | null
startRow: Number | null
endRow: Number | null
compCd: null
agencyStoreId: null
storeId: null
userId: null
category: null
userNm: null
userNmKana: null
telNo: null
fax: null
email: null
lastEditUser: null
storeGubun: null
pwCurr: null
pwdInitYn: null
apprStatCd: null
loginFailCnt: Number | null
loginFailMinYn: null
priceViewStatCd: null
groupId: null
storeLvl: null
custCd: null
builderNo: null
isLoggedIn: boolean
}

View File

@ -20,13 +20,13 @@ export type SurveyDetailInfo = {
id: number
contract_capacity: string | null
retail_company: string | null
supplementary_facilities: number | null
supplementary_facilities: string | null // number 배열
supplementary_facilities_etc: string | null
installation_system: number | null
installation_system_etc: string | null
construction_year: number | null
construction_year_etc: string | null
roof_material: number | null
roof_material: string | null // number 배열
roof_material_etc: string | null
roof_shape: number | null
roof_shape_etc: string | null
@ -74,13 +74,13 @@ export type SurveyBasicRequest = {
export type SurveyDetailRequest = {
contract_capacity: string | null
retail_company: string | null
supplementary_facilities: number | null
supplementary_facilities: string | null // number 배열
supplementary_facilities_etc: string | null
installation_system: number | null
installation_system_etc: string | null
construction_year: number | null
construction_year_etc: string | null
roof_material: number | null
roof_material: string | null // number 배열
roof_material_etc: string | null
roof_shape: number | null
roof_shape_etc: string | null