dev #758

Merged
ysCha merged 9 commits from dev into prd-deploy 2026-04-08 10:22:11 +09:00
9 changed files with 105 additions and 32 deletions

View File

@ -8,6 +8,7 @@ import { useRecoilState } from 'recoil'
import { useAxios } from '@/hooks/useAxios' import { useAxios } from '@/hooks/useAxios'
import { globalLocaleStore } from '@/store/localeAtom' import { globalLocaleStore } from '@/store/localeAtom'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import Cookies from 'js-cookie'
import GlobalSpinner from '@/components/common/spinner/GlobalSpinner' import GlobalSpinner from '@/components/common/spinner/GlobalSpinner'
@ -31,6 +32,7 @@ export default function AutoLoginPage({ autoLoginParam }) {
setIsLoading(false) setIsLoading(false)
if (response.data) { if (response.data) {
const res = response.data const res = response.data
if (res.sessionId) Cookies.set('sessionId', res.sessionId)
const result = { ...res, storeLvl: res.groupId === '60000' ? '1' : '2', pwdInitYn: 'Y' } const result = { ...res, storeLvl: res.groupId === '60000' ? '1' : '2', pwdInitYn: 'Y' }
setSession(result) setSession(result)
setSessionState(result) setSessionState(result)

View File

@ -48,6 +48,7 @@ export default function Login() {
post({ url: '/api/login/v1.0/user', data: { loginId: res.data } }).then((response) => { post({ url: '/api/login/v1.0/user', data: { loginId: res.data } }).then((response) => {
setIsLoading(false) setIsLoading(false)
if (response) { if (response) {
if (response.sessionId) Cookies.set('sessionId', response.sessionId)
const result = { ...response, storeLvl: response.groupId === '60000' ? '1' : '2', pwdInitYn: 'Y' } const result = { ...response, storeLvl: response.groupId === '60000' ? '1' : '2', pwdInitYn: 'Y' }
setSession(result) setSession(result)
setSessionState(result) setSessionState(result)
@ -107,6 +108,7 @@ export default function Login() {
if (res) { if (res) {
setIsLoading(false) setIsLoading(false)
if (res.data.result.resultCode === 'S') { if (res.data.result.resultCode === 'S') {
if (res.data.sessionId) Cookies.set('sessionId', res.data.sessionId)
setSession(res.data.data) setSession(res.data.data)
setSessionState(res.data.data) setSessionState(res.data.data)
// ID SAVE , // ID SAVE ,

View File

@ -8,7 +8,8 @@ import { useRecoilState, useRecoilValue, useResetRecoilState } from 'recoil'
import { dimmedStore, sessionStore } from '@/store/commonAtom' import { dimmedStore, sessionStore } from '@/store/commonAtom'
import { useMessage } from '@/hooks/useMessage' import { useMessage } from '@/hooks/useMessage'
import { checkSession, logout } from '@/lib/authActions' import { checkSession } from '@/lib/authActions'
import { useLogout } from '@/hooks/useLogout'
import QSelectBox from '@/components/common/select/QSelectBox' import QSelectBox from '@/components/common/select/QSelectBox'
@ -49,6 +50,7 @@ export default function Header(props) {
const [stuffSearch, setStuffSearch] = useRecoilState(stuffSearchState) const [stuffSearch, setStuffSearch] = useRecoilState(stuffSearchState)
const { closeAll } = usePopup() const { closeAll } = usePopup()
const { logoutProcess } = useLogout()
const { userSession } = props const { userSession } = props
const [sessionState, setSessionState] = useRecoilState(sessionStore) const [sessionState, setSessionState] = useRecoilState(sessionStore)
@ -372,14 +374,7 @@ export default function Header(props) {
<div className="sign-out-box"> <div className="sign-out-box">
<button <button
className="sign-out" className="sign-out"
onClick={() => { onClick={() => logoutProcess()}
setStuffSearch({
...stuffSearch,
code: 'DELETE',
})
logout()
router.replace('/login', undefined, { shallow: true })
}}
> >
{getMessage('header.logout')} {getMessage('header.logout')}
</button> </button>

View File

@ -5,10 +5,10 @@ import { sessionStore } from '@/store/commonAtom'
import { useRecoilState, useRecoilValue } from 'recoil' import { useRecoilState, useRecoilValue } from 'recoil'
import { useAxios } from '@/hooks/useAxios' import { useAxios } from '@/hooks/useAxios'
import { globalLocaleStore } from '@/store/localeAtom' import { globalLocaleStore } from '@/store/localeAtom'
import { login, logout, setSession } from '@/lib/authActions' import { login, setSession } from '@/lib/authActions'
import { useSwal } from '@/hooks/useSwal' import { useSwal } from '@/hooks/useSwal'
import { QcastContext } from '@/app/QcastProvider' import { QcastContext } from '@/app/QcastProvider'
import { useRouter } from 'next/navigation' import { useLogout } from '@/hooks/useLogout'
export default function ChangePasswordPop(props) { export default function ChangePasswordPop(props) {
const globalLocaleState = useRecoilValue(globalLocaleStore) const globalLocaleState = useRecoilValue(globalLocaleStore)
@ -19,7 +19,7 @@ export default function ChangePasswordPop(props) {
const { patch } = useAxios(globalLocaleState) const { patch } = useAxios(globalLocaleState)
const { getMessage } = useMessage() const { getMessage } = useMessage()
const [sessionState, setSessionState] = useRecoilState(sessionStore) const [sessionState, setSessionState] = useRecoilState(sessionStore)
const router = useRouter() const { logoutProcess } = useLogout()
const formInitValue = { const formInitValue = {
password1: '', password1: '',
password2: '', password2: '',
@ -129,15 +129,13 @@ export default function ChangePasswordPop(props) {
} }
} else { } else {
setIsGlobalLoading(false) setIsGlobalLoading(false)
logout() logoutProcess()
router.replace('/login', undefined, { shallow: true })
console.log('code not 200 error') console.log('code not 200 error')
} }
}) })
.catch((error) => { .catch((error) => {
setIsGlobalLoading(false) setIsGlobalLoading(false)
logout() logoutProcess()
router.replace('/login', undefined, { shallow: true })
console.log('catch::::::::', error) console.log('catch::::::::', error)
}) })
} }
@ -209,10 +207,7 @@ export default function ChangePasswordPop(props) {
<button <button
type="button" type="button"
className="btn-origin grey" className="btn-origin grey"
onClick={() => { onClick={() => logoutProcess()}
logout()
router.replace('/login', undefined, { shallow: true })
}}
> >
{getMessage('main.popup.login.btn2')} {getMessage('main.popup.login.btn2')}
</button> </button>

View File

@ -1868,7 +1868,7 @@ export default function StuffDetail() {
})} })}
{/* 라디오끝 */} {/* 라디오끝 */}
<div className="input-wrap mr5" style={{ width: '545px' }}> <div className="input-wrap mr5" style={{ width: '545px' }}>
<input type="text" className="input-light" {...form.register('objectName')} /> <input type="text" className="input-light" maxLength={50} {...form.register('objectName')} />
</div> </div>
<div className="select-wrap" style={{ width: '120px' }}> <div className="select-wrap" style={{ width: '120px' }}>
<Select <Select
@ -2461,7 +2461,7 @@ export default function StuffDetail() {
})} })}
{/* 상세라디오끝 */} {/* 상세라디오끝 */}
<div className="input-wrap mr5" style={{ width: '545px' }}> <div className="input-wrap mr5" style={{ width: '545px' }}>
<input type="text" className="input-light" {...form.register('objectName')} /> <input type="text" className="input-light" maxLength={50} {...form.register('objectName')} />
</div> </div>
<div className="select-wrap" style={{ width: '120px' }}> <div className="select-wrap" style={{ width: '120px' }}>
<Select <Select

View File

@ -799,6 +799,7 @@ export const useTrestle = () => {
const parent = canvas.getObjects().find((obj) => obj.id === surface.parentId) const parent = canvas.getObjects().find((obj) => obj.id === surface.parentId)
const { directionText, roofMaterial, moduleCompass, surfaceCompass } = parent const { directionText, roofMaterial, moduleCompass, surfaceCompass } = parent
const slope = Number(roofMaterial.pitch) const slope = Number(roofMaterial.pitch)
const angle = Number(roofMaterial.angle) || getDegreeByChon(slope)
const roofMaterialIndex = parent.roofMaterial.index const roofMaterialIndex = parent.roofMaterial.index
const { nameJp: roofMaterialIdMulti } = roofMaterial const { nameJp: roofMaterialIdMulti } = roofMaterial
const moduleSelection = moduleSelectionData?.roofConstructions?.find((construction) => construction.roofIndex === roofMaterialIndex) const moduleSelection = moduleSelectionData?.roofConstructions?.find((construction) => construction.roofIndex === roofMaterialIndex)
@ -836,7 +837,7 @@ export const useTrestle = () => {
supportMeaker, supportMeaker,
slope: +roofSizeSet === 3 ? 0 : slope, slope: +roofSizeSet === 3 ? 0 : slope,
classType: currentAngleType === 'slope' ? '0' : '1', classType: currentAngleType === 'slope' ? '0' : '1',
angle: +roofSizeSet === 3 ? 0 : getDegreeByChon(slope), angle: +roofSizeSet === 3 ? 0 : angle,
azimuth: getAzimuth(parent), azimuth: getAzimuth(parent),
moduleList, moduleList,
} }
@ -1122,7 +1123,7 @@ export const useTrestle = () => {
return return
} }
const roof = canvas.getObjects().find((obj) => obj.id === surface.parentId) const roof = canvas.getObjects().find((obj) => obj.id === surface.parentId)
const degree = getDegreeByChon(roof.roofMaterial.pitch) const degree = Number(roof.roofMaterial.angle) || getDegreeByChon(roof.roofMaterial.pitch)
rackIntvlPct = rackIntvlPct === 0 ? 1 : rackIntvlPct // 0인 경우 1로 변경 rackIntvlPct = rackIntvlPct === 0 ? 1 : rackIntvlPct // 0인 경우 1로 변경
rackIntvlPct = 100 / rackIntvlPct // 퍼센트로 변경 rackIntvlPct = 100 / rackIntvlPct // 퍼센트로 변경
const moduleLeft = lastX ?? left const moduleLeft = lastX ?? left

44
src/hooks/useLogout.js Normal file
View File

@ -0,0 +1,44 @@
import { useRecoilState, useRecoilValue } from 'recoil'
import { sessionStore } from '@/store/commonAtom'
import { globalLocaleStore } from '@/store/localeAtom'
import { stuffSearchState } from '@/store/stuffAtom'
import { useAxios } from '@/hooks/useAxios'
import { logout } from '@/lib/authActions'
import Cookies from 'js-cookie'
/**
* 로그아웃 처리 Hook
* - 로그아웃 API 호출 성공/실패 상관없이 로그아웃 처리
* - Header, ChangePasswordPop 등에서 공통으로 사용
*/
export function useLogout() {
const [sessionState] = useRecoilState(sessionStore)
const [stuffSearch, setStuffSearch] = useRecoilState(stuffSearchState)
const globalLocaleState = useRecoilValue(globalLocaleStore)
const { promisePost } = useAxios(globalLocaleState)
const logoutProcess = async () => {
const param = {
loginId: sessionState.userId,
requestId: Cookies.get('sessionId'), // 로그인 시 저장한 sessionId
}
// 로그아웃 API 호출 (실패해도 로그아웃 진행)
try {
await promisePost({ url: '/api/login/v1.0/logout', data: param })
} catch (error) {
console.log('logout api error:', error)
}
// 물건검색 상태 초기화
setStuffSearch({
...stuffSearch,
code: 'DELETE',
})
Cookies.remove('sessionId') // sessionId 쿠키 삭제
await logout() // iron-session 세션 파기
window.location.href = '/login' // 로그인 페이지로 이동 (full reload)
}
return { logoutProcess }
}

View File

@ -253,13 +253,12 @@ export default class SkeletonBuilder {
} }
private static CorrectBisectorDirection(bisector: LineParametric2d, beginNextVertex: Vertex, endPreviousVertex: Vertex, beginEdge: Edge, endEdge: Edge) { private static CorrectBisectorDirection(bisector: LineParametric2d, beginNextVertex: Vertex, endPreviousVertex: Vertex, beginEdge: Edge, endEdge: Edge) {
const beginEdge2 = beginNextVertex.PreviousEdge; // Use the vertex's actual edges — chain edges can become stale
const endEdge2 = endPreviousVertex.NextEdge; // when previous iterations of MultiSplitEvent modify the LAV
const actualBeginEdge = beginNextVertex.PreviousEdge;
const actualEndEdge = endPreviousVertex.NextEdge;
if (beginEdge !== beginEdge2 || endEdge !== endEdge2) if (actualBeginEdge.Norm.Dot(actualEndEdge.Norm) < -0.97) {
throw new Error();
if (beginEdge.Norm.Dot(endEdge.Norm) < -0.97) {
const n1 = PrimitiveUtils.FromTo(endPreviousVertex.Point, bisector.A).Normalized(); const n1 = PrimitiveUtils.FromTo(endPreviousVertex.Point, bisector.A).Normalized();
const n2 = PrimitiveUtils.FromTo(bisector.A, beginNextVertex.Point).Normalized(); const n2 = PrimitiveUtils.FromTo(bisector.A, beginNextVertex.Point).Normalized();
const bisectorPrediction = this.CalcVectorBisector(n1, n2); const bisectorPrediction = this.CalcVectorBisector(n1, n2);
@ -887,7 +886,7 @@ export default class SkeletonBuilder {
private static GetEdgeInLav(lav: CircularList<Vertex>, oppositeEdge: Edge): Vertex { private static GetEdgeInLav(lav: CircularList<Vertex>, oppositeEdge: Edge): Vertex {
for (const node of lav.Iterate()) for (const node of lav.Iterate())
if (oppositeEdge === node.PreviousEdge || if (oppositeEdge === node.PreviousEdge ||
oppositeEdge === node.Previous.Next) oppositeEdge === (node.Previous as Vertex).NextEdge)
return node; return node;
return null; return null;

View File

@ -16,6 +16,39 @@ import wallLine from '@/components/floor-plan/modal/wallLineOffset/type/WallLine
*/ */
const EPSILON = 0.1 const EPSILON = 0.1
/**
* 오목(concave) 폴리곤인지 판별합니다.
* cross product의 부호가 혼재하면 오목 폴리곤입니다.
*/
const isConcavePolygon = (points) => {
if (!points || points.length < 4) return false
let positive = 0, negative = 0
for (let i = 0; i < points.length; i++) {
const prev = points[(i - 1 + points.length) % points.length]
const curr = points[i]
const next = points[(i + 1) % points.length]
const cross = (curr.x - prev.x) * (next.y - curr.y) - (curr.y - prev.y) * (next.x - curr.x)
if (cross > 0) positive++
else if (cross < 0) negative++
}
return positive > 0 && negative > 0
}
/**
* 오목 폴리곤에만 미세한 perturbation을 적용하여 동시 이벤트(MultiSplitEvent) 방지합니다.
* 대칭적 길이(: 45.5, 45.5) 동일 거리의 이벤트를 만들어 LAV 불일치 오류를 유발하므로
* 꼭짓점에 인덱스 기반 미세 오프셋을 적용하면 이벤트가 개별 SplitEvent로 처리됩니다.
* 볼록 폴리곤은 문제가 없으므로 원본을 그대로 반환합니다.
*/
const perturbPolygonPoints = (points, eps = 1e-4) => {
if (!points || points.length < 3) return points
if (!isConcavePolygon(points)) return points
return points.map((p, i) => ({
x: p.x + (i + 1) * eps,
y: p.y + (i + 1) * eps
}))
}
export const drawSkeletonRidgeRoof = (roofId, canvas, textMode) => { export const drawSkeletonRidgeRoof = (roofId, canvas, textMode) => {
// 2. 스켈레톤 생성 및 그리기 // 2. 스켈레톤 생성 및 그리기
@ -503,11 +536,13 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
// changRoofLinePoints를 roof.skeletonPoints에 저장 (roof.points 원본은 유지) // changRoofLinePoints를 roof.skeletonPoints에 저장 (roof.points 원본은 유지)
roof.skeletonPoints = changRoofLinePoints.map(p => ({ x: p.x, y: p.y })) roof.skeletonPoints = changRoofLinePoints.map(p => ({ x: p.x, y: p.y }))
const geoJSONPolygon = toGeoJSON(changRoofLinePoints) const perturbedPoints = perturbPolygonPoints(changRoofLinePoints)
const geoJSONPolygon = toGeoJSON(perturbedPoints)
try { try {
// SkeletonBuilder는 닫히지 않은 폴리곤을 기대하므로 마지막 점 제거 // SkeletonBuilder는 닫히지 않은 폴리곤을 기대하므로 마지막 점 제거
geoJSONPolygon.pop() geoJSONPolygon.pop()
console.log('[SkeletonBuilder] geoJSONPolygon:', JSON.stringify(geoJSONPolygon, null, 2))
const skeleton = SkeletonBuilder.BuildFromGeoJSON([[geoJSONPolygon]]) const skeleton = SkeletonBuilder.BuildFromGeoJSON([[geoJSONPolygon]])
// 스켈레톤 데이터를 기반으로 내부선 생성 // 스켈레톤 데이터를 기반으로 내부선 생성