diff --git a/public/guest_profile.png b/public/guest_profile.png new file mode 100644 index 0000000..e27609d Binary files /dev/null and b/public/guest_profile.png differ diff --git a/src/app/app.html b/src/app/app.html index a18cc5f..b37b0ee 100644 --- a/src/app/app.html +++ b/src/app/app.html @@ -1,2 +1,8 @@ + + diff --git a/src/app/app.ts b/src/app/app.ts index f0769dc..d08911b 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -1,12 +1,15 @@ -import { Component } from '@angular/core'; +import { Component, inject } from '@angular/core'; import { RouterOutlet } from '@angular/router'; import { HeaderMenu } from './features/header-menu/header-menu'; +import { PopupService } from './services/popup.service'; +import { NgComponentOutlet } from '@angular/common'; @Component({ selector: 'app-root', - standalone: true, - imports: [RouterOutlet, HeaderMenu], + imports: [RouterOutlet, HeaderMenu, NgComponentOutlet], templateUrl: './app.html', styleUrl: './app.scss' }) -export class App {} +export class App { + protected popupService = inject(PopupService); +} diff --git a/src/app/components/dropdown/dropdown.scss b/src/app/components/dropdown/dropdown.scss index 576e92b..4eeb180 100644 --- a/src/app/components/dropdown/dropdown.scss +++ b/src/app/components/dropdown/dropdown.scss @@ -36,7 +36,7 @@ $dropdown_animation_duration: 0.2s; .dropdown-box { display: flex; align-items: center; - padding: 0 12px; + padding: 0 10px; height: $dropdown_height; box-sizing: border-box; gap: 8px; diff --git a/src/app/components/popup/popup.html b/src/app/components/popup/popup.html index 37d6140..b91ff1e 100644 --- a/src/app/components/popup/popup.html +++ b/src/app/components/popup/popup.html @@ -1,13 +1,17 @@ - - diff --git a/src/app/components/popup/popup.scss b/src/app/components/popup/popup.scss index e69de29..5f95ca7 100644 --- a/src/app/components/popup/popup.scss +++ b/src/app/components/popup/popup.scss @@ -0,0 +1,78 @@ +:host { + display: block; +} + +.popup-backdrop { + position: fixed; + inset: 0; + background-color: transparent; + display: flex; + justify-content: center; + align-items: center; + z-index: 100; /* .popup-outlet 내부에서의 상대적 순서 */ +} + +.popup-container { + background: white; + border-radius: 12px; + box-shadow: 0 0 40px oklch(0 0 0 / 15%); + min-width: 40px; + max-width: 90vw; + max-height: 90vh; + display: flex; + flex-direction: column; + overflow: visible; + animation: popup-enter 0.2s cubic-bezier(0.16, 1, 0.3, 1); + + border: oklch(0 0 0 / 20%) 2px solid; + gap: 15px; +} + +@keyframes popup-enter { + from { + transform: scale(0.95) translateY(10px); + opacity: 0; + } + to { + transform: scale(1) translateY(0); + opacity: 1; + } +} + +.popup-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 12px; + height: 40px; + border-bottom: 1px solid oklch(0 0 0 / 5%); + + .header-left { + display: flex; + align-items: center; + gap: 12px; + } + + .header-right { + .close-btn { + background: none; + border: none; + padding: 4px; + cursor: pointer; + border-radius: 50%; + color: oklch(0.4 0 0); + transition: background-color 0.2s, color 0.2s; + + &:hover { + background-color: oklch(0 0 0 / 5%); + color: oklch(0 0 0); + } + } + } +} + +.popup-body { + padding: 0 12px; + overflow-y: visible; + flex-grow: 1; +} diff --git a/src/app/components/popup/popup.ts b/src/app/components/popup/popup.ts index dff6d64..026cbb0 100644 --- a/src/app/components/popup/popup.ts +++ b/src/app/components/popup/popup.ts @@ -1,16 +1,18 @@ -import { Component, input, model, output } from '@angular/core'; -import { Dropdown } from '../../components/dropdown/dropdown'; -import { Switch } from '../../components/switch/switch'; +import { Component, output } from '@angular/core'; @Component({ - selector: 'popup', + selector: 'app-popup', + standalone: true, templateUrl: './popup.html', styleUrl: './popup.scss', }) export class PopupComponent { - close = output(); + /** + * 팝업 닫기 요청 (배경 클릭 또는 닫기 버튼) + */ + closeRequest = output(); onClose() { - this.close.emit() + this.closeRequest.emit(); } } diff --git a/src/app/features/header-menu/header-menu.html b/src/app/features/header-menu/header-menu.html index 90b6eff..fc65c19 100644 --- a/src/app/features/header-menu/header-menu.html +++ b/src/app/features/header-menu/header-menu.html @@ -1,6 +1,6 @@ diff --git a/src/app/features/header-menu/header-menu.scss b/src/app/features/header-menu/header-menu.scss index 73518bd..94057b2 100644 --- a/src/app/features/header-menu/header-menu.scss +++ b/src/app/features/header-menu/header-menu.scss @@ -81,5 +81,16 @@ flex-grow: 1; justify-content: flex-end; padding: 12px 20px; + + .settings { + position: relative; + z-index: 10; + cursor: pointer; + transition: transform 0.3s ease; + + &:hover { + transform: rotate(30deg); + } + } } } diff --git a/src/app/features/header-menu/header-menu.ts b/src/app/features/header-menu/header-menu.ts index 13a29fc..7257791 100644 --- a/src/app/features/header-menu/header-menu.ts +++ b/src/app/features/header-menu/header-menu.ts @@ -1,10 +1,20 @@ -import { Component } from '@angular/core'; +import { Component, inject } from '@angular/core'; import { RouterLink } from '@angular/router'; +import { NgOptimizedImage } from '@angular/common'; +import { PopupService } from '../../services/popup.service'; +import { SettingsPopup } from '../settings/settings'; @Component({ selector: 'header-menu', - imports: [RouterLink], + imports: [RouterLink, NgOptimizedImage], templateUrl: './header-menu.html', styleUrl: './header-menu.scss', }) -export class HeaderMenu {} +export class HeaderMenu { + private popupService = inject(PopupService); + + onSettingsClick() { + console.log('HeaderMenu: Settings clicked'); + this.popupService.open(SettingsPopup); + } +} diff --git a/src/app/features/settings/popup.html b/src/app/features/settings/popup.html index cd30928..059a9d3 100644 --- a/src/app/features/settings/popup.html +++ b/src/app/features/settings/popup.html @@ -1,16 +1,37 @@ - + diff --git a/src/app/features/settings/popup.scss b/src/app/features/settings/popup.scss index e69de29..8b61af1 100644 --- a/src/app/features/settings/popup.scss +++ b/src/app/features/settings/popup.scss @@ -0,0 +1,79 @@ +:host { + display: block; +} + +.profile-section { + display: flex; + align-items: center; + gap: 12px; + + font-size: 18px; + font-weight: 500; + + .avatar { + border-radius: 50%; + border: 1px solid oklch(0 0 0 / 20%); + object-fit: cover; + } +} + +.settings-content { + display: flex; + flex-direction: column; + gap: 16px; + + padding-bottom: 8px; + + section { + display: flex; + flex-direction: column; + gap: 6px; + align-items: flex-start; + justify-content: flex-start; + margin: 0; padding: 0; + border: none; + + label { + display: block; + font-weight: 500; + font-size: 14px; + color: #999999; + } + + .settings-container { + width: 100%; + height: 40px; + display: flex; + + flex-direction: row; + align-items: center; + justify-content: space-between; + + .settings-name { + display: flex; + flex-direction: row; + align-items: center; + padding-left: 10px; + gap: 8px; + + span { + display: inline-block; + font-size: 18px; + + &.settings-icon { + display: inline-block; + font-size: 24px; + + width: 24px; + height: 24px; + line-height: 24px; + } + } + } + } + + dropdown { + width: 324px; + } + } +} diff --git a/src/app/features/settings/settings.ts b/src/app/features/settings/settings.ts index 73ccdaa..34f5f5d 100644 --- a/src/app/features/settings/settings.ts +++ b/src/app/features/settings/settings.ts @@ -1,9 +1,32 @@ -import { Component } from '@angular/core'; +import { Component, inject } from '@angular/core'; +import { PopupComponent } from '../../components/popup/popup'; +import { Dropdown, DropdownOption } from '../../components/dropdown/dropdown'; +import { Switch } from '../../components/switch/switch'; +import { NgOptimizedImage } from '@angular/common'; +import { PopupService } from '../../services/popup.service'; @Component({ selector: 'settings-popup', - imports: [], + standalone: true, + imports: [PopupComponent, Dropdown, Switch, NgOptimizedImage], templateUrl: './popup.html', styleUrl: './popup.scss', }) -export class SettingsPopup {} +export class SettingsPopup { + private popupService = inject(PopupService); + + languages: DropdownOption[] = [ + { label: 'English', value: 'EN' }, + { label: '한국어', value: 'KR' }, + { label: '日本語', value: 'JP' }, + ]; + + userName: string = "Guest"; + + /** + * 팝업을 닫을 때, 자기 자신(클래스)을 명확하게 지정하여 닫습니다. + */ + close() { + this.popupService.close(SettingsPopup); + } +} diff --git a/src/app/services/popup.service.ts b/src/app/services/popup.service.ts new file mode 100644 index 0000000..bd717df --- /dev/null +++ b/src/app/services/popup.service.ts @@ -0,0 +1,42 @@ +import { Injectable, signal, Type } from '@angular/core'; + +@Injectable({ + providedIn: 'root', +}) +export class PopupService { + /** + * 현재 활성화된 팝업 컴포넌트들의 리스트 (Signal) + */ + activePopups = signal[]>([]); + + /** + * 팝업을 엽니다. 이미 열려있는 동일한 컴포넌트가 있다면 중복해서 열지 않습니다. + */ + open(component: Type) { + this.activePopups.update((popups) => { + if (popups.includes(component)) return popups; + return [...popups, component]; + }); + } + + /** + * 특정 컴포넌트를 닫습니다. + */ + close(component: Type) { + this.activePopups.update((popups) => popups.filter((p) => p !== component)); + } + + /** + * 가장 최근에 열린(마지막) 팝업을 닫습니다. + */ + closeLast() { + this.activePopups.update((popups) => popups.slice(0, -1)); + } + + /** + * 모든 팝업을 닫습니다. + */ + closeAll() { + this.activePopups.set([]); + } +} diff --git a/src/styles.scss b/src/styles.scss index 209cf4e..7e97095 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -6,29 +6,18 @@ body { font-family: 'NanumSquareNeo', 'Wanted Sans Variable', 'Pretendard JP', sans-serif; font-weight: 500; margin: 0; - overscroll-behavior-x: none; background-color: white; } .icon-24 { - font-variation-settings: - 'FILL' 0, - 'wght' 300, - 'GRAD' 0, - 'opsz' 24; - + font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 24; font-size: 24px; line-height: 24px; } .icon-40 { - font-variation-settings: - 'FILL' 0, - 'wght' 300, - 'GRAD' 0, - 'opsz' 40; - + font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 40; font-size: 40px; line-height: 40px; } @@ -60,6 +49,18 @@ p { margin: 0 auto; } +.popup-outlet { + position: fixed; + inset: 0; + pointer-events: none; + z-index: 10000; /* 최상위 레이어 */ +} + +#global-popup-root > * { + pointer-events: auto; + display: block; +} + section { margin-bottom: 40px; padding: 20px; @@ -67,3 +68,8 @@ section { border-radius: 8px; } +.container { + padding: 40px; + max-width: 600px; + margin: 0 auto; +}