Merge branch 'main' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user