Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev

This commit is contained in:
2026-02-19 09:34:30 +09:00
2 changed files with 89 additions and 123 deletions
@@ -1,27 +1,42 @@
@tailwind components;
@layer components {
/* 상단 메뉴 호버 시 하단 라인 - 기본 테마 */
.header-nav-link {
position: relative;
}
.header-nav-link::after {
content: '';
/* .header-nav-link::before {
content: "";
position: absolute;
bottom: -4px;
left: 0;
width: 0;
height: 2px;
background-color: #063A74;
background-color: #063a74;
transition: width 0.3s ease;
}
.header-nav-link:hover::after {
.header-nav-link:hover::before {
width: 100%;
} */
/* 네비게이션 아이템 사이 구분선 */
.header-nav-link::after {
content: "";
position: absolute;
left: -0.75rem;
top: 50%;
transform: translateY(-50%);
width: 1px !important;
height: 1.5rem !important;
background-color: var(--border);
}
.header-nav-link:first-child::after {
display: none;
}
[data-theme="blue"] {
header {
background: #00afe5;
@@ -35,7 +50,7 @@
color: #fff;
}
header button[role="button"].primary {
background-color: #008E8D !important;
background-color: #008e8d !important;
color: #fff !important;
}
nav a,
@@ -45,7 +60,7 @@
.header-nav-link:hover {
color: #fff !important;
}
.header-nav-link::after {
.header-nav-link::before {
background-color: #fff;
}
}
@@ -63,7 +78,7 @@
color: #fff;
}
header button[role="button"].primary {
background-color: #008E8D !important;
background-color: #008e8d !important;
color: #fff !important;
}
nav a,
@@ -73,9 +88,8 @@
.header-nav-link:hover {
color: #fff !important;
}
.header-nav-link::after {
.header-nav-link::before {
background-color: #fff;
}
}
}
@@ -21,6 +21,7 @@ import { SidebarTrigger } from "../sidebar";
import { Popover, PopoverContent, PopoverTrigger } from "../popover";
import { SearchForm } from "./search-form";
import { NoticePopover } from "./notice-popover";
import { ScrollArea, ScrollBar } from "../scroll-area";
const appConfig = getConfig();
const userId = getCookie(appConfig.AUTH_USER_ID ?? "");
@@ -71,7 +72,7 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) {
method: "POST",
credentials: "include", // ⭐ 쿠키 포함 중요!
});
if (!response.ok) {
console.error("Logout API failed");
// API 실패해도 계속 진행
@@ -82,42 +83,42 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) {
} finally {
// 2. 클라이언트 쿠키 삭제 (백업)
clearClientCookies();
// 3. 로컬/세션 스토리지 정리
if (typeof window !== "undefined") {
localStorage.clear();
sessionStorage.clear();
}
// 4. 로그인 페이지로 리다이렉트
window.location.href = "/auth/login";
}
};
// 쿠키 삭제 헬퍼 함수
const clearClientCookies = () => {
const actualHost = window.location.hostname;
// 루트 도메인 추출
const getRootDomain = (hostname: string): string => {
if (hostname === "localhost" || /^\d+\.\d+\.\d+\.\d+$/.test(hostname)) {
return hostname;
}
if (hostname.includes("hasuon.or.kr")) {
return ".hasuon.or.kr";
}
const parts = hostname.split(".");
if (parts.length >= 2) {
return `.${parts.slice(-2).join(".")}`;
}
return hostname;
};
const domain = getRootDomain(actualHost);
// 삭제할 쿠키 목록
const cookiesToDelete = [
appConfig.AUTH_USER_ID,
@@ -127,26 +128,26 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) {
"site-id-key",
"site-theme-key",
].filter(Boolean); // undefined 제거
// 여러 방법으로 쿠키 삭제 시도
cookiesToDelete.forEach((cookieName) => {
if (!cookieName) return;
// 방법 1: removeCookie 함수 사용
if (domain === "localhost") {
removeCookie(cookieName, {});
} else {
removeCookie(cookieName, { domain });
}
// 방법 2: 직접 document.cookie 설정 (도메인 포함)
if (domain !== "localhost") {
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${domain}`;
}
// 방법 3: 도메인 없이 삭제 (현재 도메인만)
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
// 방법 4: Secure 속성 포함 삭제
if (window.location.protocol === "https:") {
if (domain !== "localhost") {
@@ -156,66 +157,7 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) {
}
});
};
/*
const logout = async () => {
try {
const response = await fetch("/api/auth/logout", {
method: "POST",
});
if (!response.ok) throw new Error("Failed to logout");
const actualHost = window.location.hostname;
// 서브도메인 쿠키 삭제를 위한 루트 도메인 추출
const getRootDomain = (hostname: string): string => {
// localhost나 IP 주소인 경우
if (hostname === "localhost" || /^\d+\.\d+\.\d+\.\d+$/.test(hostname)) {
return hostname;
}
// hasuon.or.kr 도메인인 경우
if (hostname.includes("hasuon.or.kr")) {
return ".hasuon.or.kr";
}
// 일반적인 도메인의 경우 루트 도메인 추출 (예: sub.example.com → .example.com)
const parts = hostname.split(".");
if (parts.length >= 2) {
// 마지막 두 부분을 사용 (예: example.com)
return `.${parts.slice(-2).join(".")}`;
}
return hostname;
};
const domain = getRootDomain(actualHost);
const cookieOptions = domain.startsWith(".") || domain === "localhost"
? { domain }
: {};
// 쿠키 삭제 (서브도메인 포함)
if (appConfig.AUTH_USER_ID) {
removeCookie(appConfig.AUTH_USER_ID, cookieOptions);
}
if (appConfig.REFRESH_TOKEN) {
removeCookie(appConfig.REFRESH_TOKEN, cookieOptions);
}
if (appConfig.ACCESS_TOKEN) {
removeCookie(appConfig.ACCESS_TOKEN, cookieOptions);
}
if (appConfig.CLAIM_NAME) {
removeCookie(appConfig.CLAIM_NAME, cookieOptions);
}
removeCookie("site-id-key", cookieOptions);
removeCookie("site-theme-key", cookieOptions);
window.location.href = "/auth/login";
} catch (error) {
console.error(error);
}
};
*/
const openPopup = useCallback(async (popup: Popup) => {
try {
const cookieName = `cs-popup-hide-for-day-${popup.popupSn}`;
@@ -268,8 +210,8 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) {
}, []);
return (
<header className="sticky top-0 z-30 box-border flex flex-col sm:flex-row h-[6rem] w-full items-center justify-between border-b border-solid border-border bg-background">
<div className="flex items-center px-2 sm:px-4 md:px-6 w-full sm:w-auto justify-between sm:justify-start">
<header className="sticky top-0 z-30 box-border flex h-[6rem] w-full flex-col items-center justify-between border-b border-solid border-border bg-background sm:flex-row">
<div className="flex w-full items-center justify-between px-2 sm:w-auto sm:justify-start sm:px-4 md:px-6">
<div className="flex items-center gap-2 sm:gap-4">
<SidebarTrigger className="size-12 !bg-transparent p-2 hover:!bg-accent [&>svg]:!size-full [&>svg]:text-foreground" />
<h1 className="flex flex-col items-center gap-2">
@@ -291,7 +233,10 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) {
<span
className="text-[1.2rem] font-semibold text-foreground"
style={{
letterSpacing: logoWidth > 0 ? `${(logoWidth - 120) / (siteInfo?.siteNm?.length || 1)}px` : '0.05em'
letterSpacing:
logoWidth > 0
? `${(logoWidth - 120) / (siteInfo?.siteNm?.length || 1)}px`
: "0.05em",
}}
>
{siteInfo?.siteNm}
@@ -306,7 +251,9 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) {
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
className="m-0 flex h-[3rem] items-center !bg-background px-3 py-0 text-label"
>
<i className={`diveicon ${isMobileMenuOpen ? 'di-menu-close-fill' : 'di-menu-open-fill'} static text-4xl`} />
<i
className={`diveicon ${isMobileMenuOpen ? "di-menu-close-fill" : "di-menu-open-fill"} static text-4xl`}
/>
</Button>
)}
{mounted && isLoggedIn && (
@@ -314,7 +261,9 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) {
<PopoverTrigger asChild>
<Button className="group m-0 flex h-[3rem] items-center !bg-background px-2 py-0 text-label">
<i className="diveicon di-account-circle static translate-y-0.5 pr-2 text-3xl" />
<span className="text-xs hidden xs:inline">{userInfo?.userName}</span>
<span className="xs:inline hidden text-xs">
{userInfo?.userName}
</span>
<i className="diveicon di-chevron-down static p-0 transition-transform group-data-[state=open]:rotate-180" />
</Button>
</PopoverTrigger>
@@ -337,57 +286,60 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) {
</div>
{/* 모바일에서 네비게이션 (토글 시 표시) */}
{isMobileMenuOpen && (
<div className="w-full sm:hidden border-t border-border bg-background py-2">
{mounted && userSiteList?.content && userSiteList.content.length > 0 && (
<nav className="flex flex-col items-center gap-2 px-4">
<div className="w-full border-t border-border bg-background py-2 sm:hidden">
{mounted &&
userSiteList?.content &&
userSiteList.content.length > 0 && (
<nav className="flex flex-col items-center gap-2 px-4">
{userSiteList.content.map((site) => (
<a
key={site.siteId}
href={site.dmnAddr}
className="w-full py-2 text-center text-xl font-medium text-foreground transition-colors hover:text-primary"
target="_blank"
rel="noopener noreferrer"
onClick={() => setIsMobileMenuOpen(false)}
>
{site.siteNm}
</a>
))}
</nav>
)}
</div>
)}
{/* 데스크톱에서 중간 네비게이션 */}
<ScrollArea className="hidden max-w-[calc(100vw-60rem)] flex-1 items-center justify-center px-2 py-4 sm:flex">
{mounted &&
userSiteList?.content &&
userSiteList.content.length > 0 && (
<nav className="flex w-full flex-nowrap items-center justify-start gap-3 overflow-x-auto whitespace-nowrap md:gap-6">
{userSiteList.content.map((site) => (
<a
key={site.siteId}
href={site.dmnAddr}
className="text-xl font-medium text-foreground hover:text-primary transition-colors w-full text-center py-2"
className="header-nav-link whitespace-nowrap text-sm font-medium text-foreground transition-colors hover:text-primary md:text-lg lg:text-2xl"
target="_blank"
rel="noopener noreferrer"
onClick={() => setIsMobileMenuOpen(false)}
>
{site.siteNm}
</a>
))}
</nav>
)}
</div>
)}
{/* 데스크톱에서 중간 네비게이션 */}
<div className="hidden sm:flex items-center flex-1 justify-center px-2">
{mounted && userSiteList?.content && userSiteList.content.length > 0 && (
<nav className="flex items-center gap-3 md:gap-6 flex-wrap justify-center">
{userSiteList.content.map((site, index) => (
<div key={site.siteId} className="flex items-center">
<a
href={site.dmnAddr}
className="text-sm md:text-lg lg:text-2xl font-medium text-foreground hover:text-primary transition-colors header-nav-link"
target="_blank"
rel="noopener noreferrer"
>
{site.siteNm}
</a>
{index < userSiteList.content.length - 1 && (
<div className="w-px h-4 md:h-6 bg-border ml-3 md:ml-6"></div>
)}
</div>
))}
</nav>
)}
</div>
<ScrollBar orientation="horizontal" />
</ScrollArea>
{/* 데스크톱에서 오른쪽 요소들 */}
<div className="hidden sm:flex items-center gap-2 px-2 md:px-4">
<div className="hidden items-center gap-2 px-2 sm:flex md:px-4">
<SearchForm />
{mounted ? (
isLoggedIn && (
<Popover>
<PopoverTrigger asChild>
<Button className="group m-0 flex h-[3rem] items-center !bg-background px-2 md:px-4 py-0 text-label">
<i className="diveicon di-account-circle static translate-y-0.5 pr-1 md:pr-2 text-2xl md:text-3xl" />
<span className="text-sm md:text-base hidden md:inline">{userInfo?.userName}</span>
<Button className="group m-0 flex h-[3rem] items-center !bg-background px-2 py-0 text-label md:px-4">
<i className="diveicon di-account-circle static translate-y-0.5 pr-1 text-2xl md:pr-2 md:text-3xl" />
<span className="hidden text-sm md:inline md:text-base">
{userInfo?.userName}
</span>
<i className="diveicon di-chevron-down static p-0 transition-transform group-data-[state=open]:rotate-180" />
</Button>
</PopoverTrigger>