From ec23850f6a9e60478fc02f57d03f3f4847586d32 Mon Sep 17 00:00:00 2001 From: "seonkyu.kim" Date: Tue, 5 Nov 2024 15:23:12 +0900 Subject: [PATCH 01/17] =?UTF-8?q?[=F0=9F=93=9D]=20README.md=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 968bc44..b9ef3f5 100644 --- a/README.md +++ b/README.md @@ -5,4 +5,4 @@ - Blazor WebAssembly - HTML & CSS ### IDE -- JetBrains Rider \ No newline at end of file +- JetBrains Rider From 32af4df1ec6b772e5e91aea7a93ce69c94e6d5e4 Mon Sep 17 00:00:00 2001 From: Seonkyu_Kim Date: Mon, 14 Apr 2025 10:20:21 +0900 Subject: [PATCH 02/17] =?UTF-8?q?[=F0=9F=93=9D]=20.gitignore=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 842829d..2400df5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,17 @@ # 특정 환경에 따라 추가 +/private/ +/publish/ +/bin/ +/obj/ + +./private/ ./privacy/ ./publish/ -publish/ ./bin/ + # 기본 파일 및 폴더 제외 *.log *.env @@ -14,6 +20,7 @@ publish/ *.swp # macOS 관련 파일 제외 +._ ._* .DS_Store .AppleDouble From 5a5e28da82fcc0ef673b527e9cbcd6f6d6758275 Mon Sep 17 00:00:00 2001 From: Seonkyu_Kim Date: Wed, 16 Apr 2025 17:37:55 +0900 Subject: [PATCH 03/17] =?UTF-8?q?[=F0=9F=93=9D]=20=EB=A1=9C=EC=BB=AC=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9C=84=ED=95=9C=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=EB=A9=94=EC=84=B8=EC=A7=80=20=EC=A4=91=EB=8B=A8=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Properties/launchSettings.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json index 62302d1..45e290c 100644 --- a/Properties/launchSettings.json +++ b/Properties/launchSettings.json @@ -12,8 +12,8 @@ "http": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, - "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "launchBrowser": false, +// "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "applicationUrl": "http://localhost:5024", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" @@ -22,8 +22,8 @@ "https": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, - "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "launchBrowser": false, +// "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "applicationUrl": "https://localhost:7274;http://localhost:5024", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" @@ -31,8 +31,8 @@ }, "IIS Express": { "commandName": "IISExpress", - "launchBrowser": true, - "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "launchBrowser": false, +// "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } From f28e292d1aa17046f66782b22df103fc720e2a11 Mon Sep 17 00:00:00 2001 From: Seonkyu_Kim Date: Thu, 17 Apr 2025 10:59:15 +0900 Subject: [PATCH 04/17] =?UTF-8?q?[=F0=9F=93=9D]=20README.md=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20-=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 968bc44..2c80733 100644 --- a/README.md +++ b/README.md @@ -5,4 +5,61 @@ - Blazor WebAssembly - HTML & CSS ### IDE -- JetBrains Rider \ No newline at end of file +- JetBrains Rider + +--- + +## Project Rule +### 구조 < MVVM > +#### Model +- 데이터를 정의하는 역할 +- ./Program/Models 에 위치 + - 별도의 DTO 와 엔티티의 클래스로 구성 +#### View +- UI를 정의하는 역할 +- ./Program/Pages 에 .razor 파일로 선언 + - .razor.css 파일로 스타일을 정의 + - .razor.cs 파일은 View와 Viewmodel을 연결하는 역할만 진행 + - 필요시 하위 디렉토리를 만들어 기능별로 묶어서 사용 할 수 있음 +#### ViewModel +- UI와 데이터를 연결하는 역할 +- ./Program/ViewModels 에 .cs 파일로 선언 + - VM.cs 파일은 연결하려는 view.razor 의 view와 같은 이름으로 선언해야 함 + - ViewModel은 View와 Model을 연결만 하는 것이 아닌 실제적인 역할을 수행 + - ViewModel의 연결 없이 View에서 직접 Model을 사용하여 데이터를 가져올 수 없음 + - View에 여러개의 ViewModel이 연결될 수 있음 + - 가능한 ViewModel과 View를 1:1로 연결하는 것을 권장 + - 테스트를 고려해 ViewModel은 UI 코드에 의존하지 않도록 작성 + +### 그 외 +#### 1. 네임스페이스 구조 +- 폴더 구조와 네임스페이스는 반드시 일치해야 함 +``` +/Program/Pages/Login.razor +namespace Front.Program.Pages; +``` +#### 2. Service 를 활용한 별도의 계층 구분 +- 외부의 API와 통신하는 부분은 ViewModel 에서 직접적인 API 호출이 아닌 별도의 Service를 통해 호출 + +#### 3. 상태 변경과 UI 갱신을 분리 +- View.razor.cs 에서 ViewModel을 통해 상태 변경과 함께 View를 다시 그리는것이 아님 + - ViewModel에서 상태 변경을 하고 ViewModel의 PropertyChanged 이벤트를 통해 View에 알림 + - ViewModle에서 상태 변경시에 INotifyPropertyChanged 방식을 사용 + +#### 4. 상태 공유 규칙 +- View간 전역 상태를 관리하는 경우에 'AppState' 를 사용하여 관리 + - 해당 클래스가 많아질 경우 도메인 접두어를 붙여서 사용 + - AppState는 ViewModel과 Service에서 사용 가능 + - AppState는 ViewModel과 Service에서 DI를 통해 주입받아 사용 Program.cs 에서 DI를 등록 + - appState는 Scoped으로 등록 + - AppState는 ViewModel과 Service에서 직접적으로 상태를 변경하지 않도록 작성 + - ViewModel과 Service에서 AppState의 상태를 변경하는 경우에는 반드시 AppState의 메서드를 통해서만 변경하도록 작성 + - AppState는 ViewModel과 Service에서 직접적으로 상태를 가져오는 경우에는 반드시 AppState의 메서드를 통해서만 가져오도록 작성 + - AppState에서 상태를 변경시에 event Action? OnChange 방식을 사용 + +#### 5. DI +- ViewModel 과 Service는 DI를 통해 주입받아 사용하며 Program.cs 에서 DI를 등록 +- ViewModel은 가급적으로 transient로 등록 + - 하지만 한 View에서 여러 컴포넌트가 하나의 상태를 공유해야 할 경우에는 Scoped로 등록 +- Service는 Scoped으로 등록 + - Service는 ViewModel과 다르게 여러 컴포넌트에서 같은 Service를 사용해야 하는 경우가 많기 때문에 Scoped으로 등록 From 13924e29035e9354991654a11e912e2001f81a89 Mon Sep 17 00:00:00 2001 From: Seonkyu_Kim Date: Fri, 18 Apr 2025 13:00:38 +0900 Subject: [PATCH 05/17] =?UTF-8?q?[=F0=9F=93=9D]=20Project=20Rule=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Documents/Project RULE 10.md | 200 +++++++++++++++++++++++++++++++++++ README.md | 60 ++--------- 2 files changed, 207 insertions(+), 53 deletions(-) create mode 100644 Documents/Project RULE 10.md diff --git a/Documents/Project RULE 10.md b/Documents/Project RULE 10.md new file mode 100644 index 0000000..1b58abd --- /dev/null +++ b/Documents/Project RULE 10.md @@ -0,0 +1,200 @@ +# Coding RULE 10 + +## 1️⃣ 프로젝트 구조 +### 🔹 MVVM 구조 +#### Model +- 데이터를 정의하는 역할 +- ./Program/Models 에 위치 + - 별도의 DTO 와 엔티티의 클래스로 구성 +#### View +- UI를 정의하는 역할 +- ./Program/Views 에 .razor 파일로 선언 + - .razor.css 파일로 스타일을 정의 + - .razor.cs 파일은 View와 ViewModel을 연결하는 역할만 진행 + - 필요시 하위 디렉토리를 만들어 기능별로 묶어서 사용 할 수 있음 +#### ViewModel +- UI와 데이터를 연결하는 역할 +- ./Program/ViewModels 에 .cs 파일로 선언 + - VM.cs 파일은 연결하려는 view.razor 의 view와 같은 이름으로 선언해야 함 + - ViewModel은 View와 Model을 연결만 하는 것이 아닌 실제적인 역할을 수행 + - ViewModel의 연결 없이 View에서 직접 Model을 사용하여 데이터를 가져올 수 없음 + - View에 여러개의 ViewModel이 연결될 수 있음 + - 가능한 ViewModel과 View를 1:1로 연결하는 것을 권장 + - 테스트를 고려해 ViewModel은 UI 코드에 의존하지 않도록 작성 + +### 🔹 네임스페이스 구조 +- 폴더 구조와 네임스페이스는 반드시 일치해야 함 +```csharp +/Program/Pages/Login.razor +namespace Front.Program.Pages; +``` + +### 🔹 DI +- ViewModel 과 Service는 DI를 통해 주입받아 사용하며 Program.cs 에서 DI를 등록 +- ViewModel은 가급적으로 transient로 등록 + - 하지만 한 View에서 여러 컴포넌트가 하나의 상태를 공유해야 할 경우에는 Scoped로 등록 +- Service는 Scoped으로 등록 + - Service는 ViewModel과 다르게 여러 컴포넌트에서 같은 Service를 사용해야 하는 경우가 많기 때문에 Scoped으로 등록 +--- + +## 2️⃣ 코드 스타일 가이드 +- [C# 코드 스타일 권장 사항 (Microsoft Docs)]("https://learn.microsoft.com/ko-kr/dotnet/csharp/fundamentals/coding-style/coding-conventions")을 따른다. +- 클래스, 인터페이스, 메서드, 변수, 상수 등 모든 명명 및 스타일은 위 문서를 기준으로 작성한다. +- **필요**시 `.editorconfig` 파일을 통해 프로젝트 전체에 스타일 자동 적용 도구를 사용할 수 있다. +--- + +## 3️⃣ Git 규칙 +### 🔹 Git Repository +- 실제 팀 Git의 저장소를 사용하여 관리하는 것이 아닌 해당 저장소를 Fork 하여 사용한다. +- Fork한 저장소는 개인의 Gitea 계정에 저장된다. +- 모든 작업은 Fork한 개인저장소에서 진행한다. +- 개인 최종 작업이 끝난 후에 팀 저장소에 Pull Request를 요청한다. + - 이때 PR 작업의 브랜치는 아래 브랜치 규칙에 따라 선택한다. + - PR 작업의 규칙은 아래 PR 규칙을 따른다. + - PR시 반드시 리뷰어의 요청을 통해 머지를 진행한다. + +### 🔹 브랜치 규칙 +| branch | Description | e.g. | +|:---------:|:-------------:|:-----------------:| +| main | 실제 운영되는 최종 코드 | 배포 전 최종 QA 후 머지 | +| dev | 개발 통합 브랜치 | 모든 feature 브랜치 PR | +| feature/* | 신기능 개발 브랜치 | feature/login | +| bugfix/* | 버그 수정 브랜치 | bugfix/login | +|hotfix/* | 긴급 수정 브랜치 | hotfix/login | +|refactor/*| 리팩토링 브랜치 | refactor/login | + +- feature 브랜치는 기능 단위로 나누어 작업하며, 기능이 완료되면 dev 브랜치에 PR을 요청한다. +- dev 브랜치는 모든 feature 브랜치의 통합 브랜치로, QA 후 main 브랜치에 머지된다. +- feature와 bugfix 브랜치는 dev 브랜치를 기준으로 생성한다. +- hotfix 브랜치는 main 브랜치를 기준으로 생성한다. +- refactor 브랜치는 리팩토링 작업을 위한 브랜치로, 기능 추가나 버그 수정과는 별도로 관리한다. +- 브랜치 이름은 소문자로 작성하며, 단어는 '-'로 구분한다. +- 브랜치 이름은 기능이나 버그 수정의 내용을 간결하게 나타내고 최대 3~4단어로 구성하며, 의미가 명확해야 한다. + +### 🔹 커밋 메시지 규칙 +| Type | Name | Description | +|:----:|:--------:|:--------------------------------------| +| [📝] | Document | 문서 추가, 문서 수정 등 | +| [✨] | feature | 새로운 기능의 추가 | +| [🔥] | fire | 코드나 문서 등의 삭제 | +| [🐛] | bug fix | 버그 수정 | +| [🎨] |STYLE| 코드 포맷팅, 주석, 공백 등 (기능 변경 없음) | +| [♻️] | REFACTOR | 코드 리팩토링 | +| [✅] | TEST | 테스트 코드 추가 | +| [📁] | CHORE| 빌드 업무, 패키지 매니저 수정 등 (.gitignore 수정 등) | + +- 커밋 메시지의 작성은 "[imoge] Title - Description" 형식으로 작성한다. + - e.g. "[✨] 로그인 기능 추가 - 로그인 기능을 추가했습니다." +- Title은 10자 이내로 작성하며, 언어의 제약 없이 작성한다. +- Title은 간결하고 명확하게 작성한다. +- Description은 50자 이내로 작성하며, 언어의 제약 없이 작성한다. +- Description은 Title을 보완하는 내용을 작성한다. +- Description은 선택 사항으로, 필요에 따라 작성한다. + +### 🔹 PR(Pull Request) 규칙 +- PR 제목은 커밋 규칙을 따르되 이슈를 수행하는 경우에는 이슈 번호를 포함한다. + - "[#이슈번호] Title" +- 기능 단위로 PR을 분리 (작업이 너무 크면 나누기) +- 최소 1인 이상의 리뷰어의 코드 리뷰 후 dev 브랜치로 머지 +- 충돌이 없고 빌드가 통과된 경우에만 머지 허용 +- PR 본문에 해당 작업의 목적과 결과 요약 작성 +--- + +## 4️⃣ 작업 프로세스 +### 🔹 작업 단위 기준 +- 하나의 브랜치는 하나의 기능/이슈 작업 단위로 구성한다. +- 작업 시작 전, Gitea Issue 또는 Notion Task로 정의된 항목을 확인한다. + +### 🔹 이슈 추적 방식 +- 모든 작업은 Gitea Issue 또는 별도 트래킹 시스템을 통해 관리한다. +- 이슈 번호는 PR 제목에 포함한다. +- 이슈는 작업이 완료되면 반드시 닫는다. + - Closes #42 / Fixes #42 / Resolves #42 등으로 PR 본문에 넣어 닫는 방식도 사용할 수 있다. + +### 🔹 커뮤니케이션 흐름 +- 작업 시작 → 진행 중 → 완료 상태를 태그나 칸반 보드로 표현한다. + +--- + +## 5️⃣ 협업 및 커뮤니케이션 +- 추후 팀이 늘어남에 따라 추가 예정 +--- + +## 6️⃣ 테스트 및 품질 관리 +- 추후 프로젝트의 규모에 따라 추가 예정 +--- + +## 7️⃣ 보안 및 인증 관련 +### 🔹 보안 관련 +- 민감한 정보는 절대 Git에 커밋하지 않는다. + - ./private 폴더를 로컬 저장소에 생성하여 비밀 정보를 관리한다. (ignore 폴더) +- API 인증은 JWT를 사용하며, 모든 API 요청 시 JWT 토큰을 헤더에 포함하여 전송한다. +- FE 에서는 절대 비밀 정보를 노출하지 않도록 한다. + - API 키, 비밀번호 등은 절대 코드에 하드코딩하지 않는다. + - API 키는 환경 변수나 설정 파일을 통해 관리한다. +- API 요청 시 CORS 정책을 준수한다. + - API 서버와 클라이언트 서버의 도메인이 다를 경우 CORS 설정을 통해 허용된 도메인에서만 요청을 받을 수 있도록 한다. + - CORS 설정은 API 서버에서 관리하며, 클라이언트에서는 별도로 설정하지 않는다. + + +--- + +## 8️⃣ 배포 및 운영 +### 🔹 배포 전략 +- 배포는 Jenkins를 통해 자동화되어 있으며, Gitea 저장소와 연동되어 특정 브랜치에 대한 변경을 자동 감지하고 실행된다. +- 개발자는 별도의 수동 빌드나 배포를 할 필요 없이, 브랜치 전략과 PR 규칙을 따르면 된다. + +### 🔹 브랜치 기반 배포 흐름 +|브랜치|용도| 대상 환경| +|:---:|:---:|:----| +|dev|통합 테스트용|Debug 환경 (내부 QA)| +|main|운영 서비스용|Release 환경 (실서비스)| +- `feature/*`, `bugfix/*` 브랜치는 배포되지 않으며, PR을 통해 dev 또는 main 으로 `머지될 때만 배포`가 진행된다. +- 빌드는 .NET 8 기반으로 진행되며, 결과물은 자동으로 컨테이너에서 실행된다. + +### 🔹 배포 자동화 요약 +- PR 머지 시 Jenkins가 자동으로: + 1. 빌드 실행 + 2. 기존 컨테이너 종료 후 새 컨테이너 실행 +- 개발자는 PR만 규칙에 따라 작성하면 자동으로 배포까지 완료된다. + +### 🔹 기타 참고 사항 +- 환경 설정 및 보안 설정은 팀 내 인프라 관리자가 관리하며, 개발자는 직접 접근하지 않는다. +- Front(WebAssembly)는 별도 서버 없이 백엔드가 static 파일로 제공한다. +- 배포 실패 시 인프라 관리자가 수동 복구하며, 개발자는 따로 조치할 필요 없음 + +--- + +## 9️⃣ 공통 코드 및 재사용 정책 +### 🔹 Service 를 활용한 별도의 계층 구분 +- 외부의 API와 통신하는 부분은 ViewModel 에서 직접적인 API 호출이 아닌 별도의 Service를 통해 호출 + +### 🔹 상태 공유 규칙 +- View간 전역 상태를 관리하는 경우에 'AppState' 를 사용하여 관리 + - 해당 클래스가 많아질 경우 도메인 접두어를 붙여서 사용 + - AppState는 ViewModel과 Service에서 사용 가능 + - AppState는 ViewModel과 Service에서 DI를 통해 주입받아 사용 Program.cs 에서 DI를 등록 + - appState는 Scoped으로 등록 + - AppState는 ViewModel과 Service에서 직접적으로 상태를 변경하지 않도록 작성 + - ViewModel과 Service에서 AppState의 상태를 변경하는 경우에는 반드시 AppState의 메서드를 통해서만 변경하도록 작성 + - AppState는 ViewModel과 Service에서 직접적으로 상태를 가져오는 경우에는 반드시 AppState의 메서드를 통해서만 가져오도록 작성 + - AppState에서 상태를 변경시에 event Action? OnChange 방식을 사용 + +--- + +## 🔟 예외 처리 및 로깅 +### 🔹 상태 변경과 UI 갱신을 분리 +- View.razor.cs 에서 ViewModel을 통해 상태 변경과 함께 View를 다시 그리는것이 아님 + - ViewModel에서 상태 변경을 하고 ViewModel의 PropertyChanged 이벤트를 통해 View에 알림 + - ViewModel에서 상태 변경시에 INotifyPropertyChanged 방식을 사용 + +### 🔹 에러 처리 +- API 요청 시 에러가 발생할 경우, 에러 메시지를 사용자에게 노출하지 않는다. + - 에러 메시지는 로그에 기록하고, 사용자에게는 일반적인 에러 메시지를 노출한다. + - 예) "서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요." +- API 요청 시 에러가 발생할 경우, 에러 코드를 반환한다. + - 에러 코드는 HTTP 상태 코드와 함께 반환하며, 클라이언트에서는 에러 코드를 기반으로 처리한다. + - 예) 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found 등 +- ViewModel에서는 사용자 입력 예외, API 실패처리를 UI와 연결하여 처리한다. +- 공통 예외 응답은 Service에서 처리하며, ViewModel에서는 공통 예외 응답을 사용하여 UI와 연결한다. +- 사용자에 대한 알림 방식은 AlertService를 통해 처리한다. \ No newline at end of file diff --git a/README.md b/README.md index 2c80733..4313c70 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,12 @@ # Web Client +## Project Rule + +### [[📚 PROJECT RULE (클릭시 이동)]](Documents/Project%20RULE%2010.md) +***필수 사항은 반드시 지켜주세요!*** + +--- + ## Development Environment ### Skill - Blazor WebAssembly @@ -9,57 +16,4 @@ --- -## Project Rule -### 구조 < MVVM > -#### Model -- 데이터를 정의하는 역할 -- ./Program/Models 에 위치 - - 별도의 DTO 와 엔티티의 클래스로 구성 -#### View -- UI를 정의하는 역할 -- ./Program/Pages 에 .razor 파일로 선언 - - .razor.css 파일로 스타일을 정의 - - .razor.cs 파일은 View와 Viewmodel을 연결하는 역할만 진행 - - 필요시 하위 디렉토리를 만들어 기능별로 묶어서 사용 할 수 있음 -#### ViewModel -- UI와 데이터를 연결하는 역할 -- ./Program/ViewModels 에 .cs 파일로 선언 - - VM.cs 파일은 연결하려는 view.razor 의 view와 같은 이름으로 선언해야 함 - - ViewModel은 View와 Model을 연결만 하는 것이 아닌 실제적인 역할을 수행 - - ViewModel의 연결 없이 View에서 직접 Model을 사용하여 데이터를 가져올 수 없음 - - View에 여러개의 ViewModel이 연결될 수 있음 - - 가능한 ViewModel과 View를 1:1로 연결하는 것을 권장 - - 테스트를 고려해 ViewModel은 UI 코드에 의존하지 않도록 작성 -### 그 외 -#### 1. 네임스페이스 구조 -- 폴더 구조와 네임스페이스는 반드시 일치해야 함 -``` -/Program/Pages/Login.razor -namespace Front.Program.Pages; -``` -#### 2. Service 를 활용한 별도의 계층 구분 -- 외부의 API와 통신하는 부분은 ViewModel 에서 직접적인 API 호출이 아닌 별도의 Service를 통해 호출 - -#### 3. 상태 변경과 UI 갱신을 분리 -- View.razor.cs 에서 ViewModel을 통해 상태 변경과 함께 View를 다시 그리는것이 아님 - - ViewModel에서 상태 변경을 하고 ViewModel의 PropertyChanged 이벤트를 통해 View에 알림 - - ViewModle에서 상태 변경시에 INotifyPropertyChanged 방식을 사용 - -#### 4. 상태 공유 규칙 -- View간 전역 상태를 관리하는 경우에 'AppState' 를 사용하여 관리 - - 해당 클래스가 많아질 경우 도메인 접두어를 붙여서 사용 - - AppState는 ViewModel과 Service에서 사용 가능 - - AppState는 ViewModel과 Service에서 DI를 통해 주입받아 사용 Program.cs 에서 DI를 등록 - - appState는 Scoped으로 등록 - - AppState는 ViewModel과 Service에서 직접적으로 상태를 변경하지 않도록 작성 - - ViewModel과 Service에서 AppState의 상태를 변경하는 경우에는 반드시 AppState의 메서드를 통해서만 변경하도록 작성 - - AppState는 ViewModel과 Service에서 직접적으로 상태를 가져오는 경우에는 반드시 AppState의 메서드를 통해서만 가져오도록 작성 - - AppState에서 상태를 변경시에 event Action? OnChange 방식을 사용 - -#### 5. DI -- ViewModel 과 Service는 DI를 통해 주입받아 사용하며 Program.cs 에서 DI를 등록 -- ViewModel은 가급적으로 transient로 등록 - - 하지만 한 View에서 여러 컴포넌트가 하나의 상태를 공유해야 할 경우에는 Scoped로 등록 -- Service는 Scoped으로 등록 - - Service는 ViewModel과 다르게 여러 컴포넌트에서 같은 Service를 사용해야 하는 경우가 많기 때문에 Scoped으로 등록 From 0c7bdeb8d5b0de263d35ac3425266fd5324a34ef Mon Sep 17 00:00:00 2001 From: Seonkyu_Kim Date: Fri, 18 Apr 2025 16:46:34 +0900 Subject: [PATCH 06/17] =?UTF-8?q?[=F0=9F=8E=A8]=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Documents/Project RULE 10.md | 29 ++++++++++++++++++- Front.csproj | 19 ++++++++++++ {Layout => Program/Layout}/MainLayout.razor | 0 .../Layout}/MainLayout.razor.css | 0 {Layout => Program/Layout}/NavMenu.razor | 0 {Layout => Program/Layout}/NavMenu.razor.css | 0 {Pages => Program/Views}/Counter.razor | 0 {Pages => Program/Views}/Home.razor | 2 ++ {Pages => Program/Views}/Weather.razor | 0 _Imports.razor | 2 +- 10 files changed, 50 insertions(+), 2 deletions(-) rename {Layout => Program/Layout}/MainLayout.razor (100%) rename {Layout => Program/Layout}/MainLayout.razor.css (100%) rename {Layout => Program/Layout}/NavMenu.razor (100%) rename {Layout => Program/Layout}/NavMenu.razor.css (100%) rename {Pages => Program/Views}/Counter.razor (100%) rename {Pages => Program/Views}/Home.razor (95%) rename {Pages => Program/Views}/Weather.razor (100%) diff --git a/Documents/Project RULE 10.md b/Documents/Project RULE 10.md index 1b58abd..e1f3934 100644 --- a/Documents/Project RULE 10.md +++ b/Documents/Project RULE 10.md @@ -1,5 +1,28 @@ # Coding RULE 10 +## 목차 +[1️⃣ 프로젝트 구조](#1-프로젝트-구조) + +[2️⃣ 코드 스타일 가이드](#2-코드-스타일-가이드) + +[3️⃣ Git 규칙](#3-git-규칙) + +[4️⃣ 작업 프로세스](#4-작업-프로세스) + +[5️⃣ 협업 및 커뮤니케이션](#5-협업-및-커뮤니케이션) + +[6️⃣ 테스트 및 품질 관리](#6-테스트-및-품질-관리) + +[7️⃣ 보안 및 인증 관련](#7-보안-및-인증-관련) + +[8️⃣ 배포 및 운영](#8-배포-및-운영) + +[9️⃣ 공통 코드 및 재사용 정책](#9-공통-코드-및-재사용-정책) + +[🔟 예외 처리 및 로깅](#10-예외-처리-및-로깅) + +--- + ## 1️⃣ 프로젝트 구조 ### 🔹 MVVM 구조 #### Model @@ -35,12 +58,14 @@ namespace Front.Program.Pages; - 하지만 한 View에서 여러 컴포넌트가 하나의 상태를 공유해야 할 경우에는 Scoped로 등록 - Service는 Scoped으로 등록 - Service는 ViewModel과 다르게 여러 컴포넌트에서 같은 Service를 사용해야 하는 경우가 많기 때문에 Scoped으로 등록 + --- ## 2️⃣ 코드 스타일 가이드 - [C# 코드 스타일 권장 사항 (Microsoft Docs)]("https://learn.microsoft.com/ko-kr/dotnet/csharp/fundamentals/coding-style/coding-conventions")을 따른다. - 클래스, 인터페이스, 메서드, 변수, 상수 등 모든 명명 및 스타일은 위 문서를 기준으로 작성한다. - **필요**시 `.editorconfig` 파일을 통해 프로젝트 전체에 스타일 자동 적용 도구를 사용할 수 있다. + --- ## 3️⃣ Git 규칙 @@ -98,6 +123,7 @@ namespace Front.Program.Pages; - 최소 1인 이상의 리뷰어의 코드 리뷰 후 dev 브랜치로 머지 - 충돌이 없고 빌드가 통과된 경우에만 머지 허용 - PR 본문에 해당 작업의 목적과 결과 요약 작성 + --- ## 4️⃣ 작업 프로세스 @@ -118,10 +144,12 @@ namespace Front.Program.Pages; ## 5️⃣ 협업 및 커뮤니케이션 - 추후 팀이 늘어남에 따라 추가 예정 + --- ## 6️⃣ 테스트 및 품질 관리 - 추후 프로젝트의 규모에 따라 추가 예정 + --- ## 7️⃣ 보안 및 인증 관련 @@ -136,7 +164,6 @@ namespace Front.Program.Pages; - API 서버와 클라이언트 서버의 도메인이 다를 경우 CORS 설정을 통해 허용된 도메인에서만 요청을 받을 수 있도록 한다. - CORS 설정은 API 서버에서 관리하며, 클라이언트에서는 별도로 설정하지 않는다. - --- ## 8️⃣ 배포 및 운영 diff --git a/Front.csproj b/Front.csproj index 4110bed..169a8d5 100644 --- a/Front.csproj +++ b/Front.csproj @@ -13,5 +13,24 @@ + + + + + + + + + + <_ContentIncludedByDefault Remove="Pages\Counter.razor" /> + <_ContentIncludedByDefault Remove="Pages\Home.razor" /> + <_ContentIncludedByDefault Remove="Pages\Weather.razor" /> + + + + + + + diff --git a/Layout/MainLayout.razor b/Program/Layout/MainLayout.razor similarity index 100% rename from Layout/MainLayout.razor rename to Program/Layout/MainLayout.razor diff --git a/Layout/MainLayout.razor.css b/Program/Layout/MainLayout.razor.css similarity index 100% rename from Layout/MainLayout.razor.css rename to Program/Layout/MainLayout.razor.css diff --git a/Layout/NavMenu.razor b/Program/Layout/NavMenu.razor similarity index 100% rename from Layout/NavMenu.razor rename to Program/Layout/NavMenu.razor diff --git a/Layout/NavMenu.razor.css b/Program/Layout/NavMenu.razor.css similarity index 100% rename from Layout/NavMenu.razor.css rename to Program/Layout/NavMenu.razor.css diff --git a/Pages/Counter.razor b/Program/Views/Counter.razor similarity index 100% rename from Pages/Counter.razor rename to Program/Views/Counter.razor diff --git a/Pages/Home.razor b/Program/Views/Home.razor similarity index 95% rename from Pages/Home.razor rename to Program/Views/Home.razor index 9001e0b..9dde77b 100644 --- a/Pages/Home.razor +++ b/Program/Views/Home.razor @@ -5,3 +5,5 @@

Hello, world!

Welcome to your new app. + +15 diff --git a/Pages/Weather.razor b/Program/Views/Weather.razor similarity index 100% rename from Pages/Weather.razor rename to Program/Views/Weather.razor diff --git a/_Imports.razor b/_Imports.razor index 6e0e2c3..9e75e1b 100644 --- a/_Imports.razor +++ b/_Imports.razor @@ -7,4 +7,4 @@ @using Microsoft.AspNetCore.Components.WebAssembly.Http @using Microsoft.JSInterop @using Front -@using Front.Layout +@using Front.Program.Layout From d31369f49c6197f26a58cb5bdecc7e5a5d4bc856 Mon Sep 17 00:00:00 2001 From: Seonkyu_Kim Date: Mon, 21 Apr 2025 17:57:25 +0900 Subject: [PATCH 07/17] =?UTF-8?q?[=F0=9F=93=9D]=20Blazor=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=B6=84=EC=84=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wwwroot/index.html | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/wwwroot/index.html b/wwwroot/index.html index b3dffd4..10f3555 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -2,23 +2,37 @@ + + + Front + + + + + - + + + +
+
+ +
@@ -26,6 +40,7 @@ Reload 🗙
+ From deba36e3d5aa8c40eafa60497cdd691b9b43fdbf Mon Sep 17 00:00:00 2001 From: Seonkyu_Kim Date: Tue, 22 Apr 2025 17:38:12 +0900 Subject: [PATCH 08/17] =?UTF-8?q?[=E2=9C=A8]=20MainLayout=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App.razor | 6 +- Program/Layout/MainLayout.razor | 18 +++-- Program/Layout/MainLayout.razor.css | 118 ++++++++++++++++------------ Program/Layout/TopMenu.razor | 1 + Program/Layout/TopMenu.razor.css | 0 wwwroot/index.html | 2 +- 6 files changed, 86 insertions(+), 59 deletions(-) create mode 100644 Program/Layout/TopMenu.razor create mode 100644 Program/Layout/TopMenu.razor.css diff --git a/App.razor b/App.razor index 6fd3ed1..b43127c 100644 --- a/App.razor +++ b/App.razor @@ -1,8 +1,10 @@  + - - + + + Not found diff --git a/Program/Layout/MainLayout.razor b/Program/Layout/MainLayout.razor index 76eb725..1d07228 100644 --- a/Program/Layout/MainLayout.razor +++ b/Program/Layout/MainLayout.razor @@ -1,16 +1,24 @@ @inherits LayoutComponentBase
- diff --git a/Program/Layout/MainLayout.razor.css b/Program/Layout/MainLayout.razor.css index ecf25e5..74ffa3a 100644 --- a/Program/Layout/MainLayout.razor.css +++ b/Program/Layout/MainLayout.razor.css @@ -1,77 +1,93 @@ .page { - position: relative; display: flex; flex-direction: column; -} - -main { - flex: 1; -} - -.sidebar { - background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); + height: 100vh; + overflow: hidden; } .top-row { - background-color: #f7f7f7; - border-bottom: 1px solid #d6d5d5; - justify-content: flex-end; + position: fixed; + top: 0; + left: 0; + right: 0; height: 3.5rem; display: flex; align-items: center; + background-color: #f7f7f7; + border-bottom: 1px solid #ddd; + z-index: 100; + padding: 0 1rem; } - .top-row ::deep a, .top-row ::deep .btn-link { - white-space: nowrap; - margin-left: 1.5rem; - text-decoration: none; - } +.top-title { + width: 250px; + font-weight: bold; +} - .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { - text-decoration: underline; - } +.top-menu { + flex: 1; + display: flex; + justify-content: flex-end; + gap: 1rem; +} - .top-row ::deep a:first-child { - overflow: hidden; - text-overflow: ellipsis; - } +/* 본문 구조 */ +main { + display: flex; + flex: 1; + margin-top: 3.5rem; + overflow: hidden; + /*height: calc(100vh - 3.5rem);*/ +} -@media (max-width: 640.98px) { +.side-menu { + width: 250px; + background: linear-gradient(180deg, rgb(5, 39, 103), #3a0647); + color: white; + padding: 1rem; + overflow-y: auto; + flex-shrink: 0; +} + +.content { + flex: 1; + padding: 1.5rem; + overflow-y: auto; + background-color: #fff; +} + +/* ✅ 모바일 대응 */ +@media (max-width: 768px) { .top-row { + flex-direction: row; justify-content: space-between; } - .top-row ::deep a, .top-row ::deep .btn-link { - margin-left: 0; + .top-title { + width: auto; + font-size: 1rem; } -} -@media (min-width: 641px) { - .page { + .top-menu { + justify-content: flex-end; + } + + main { + flex-direction: column; + height: calc(100vh - 3.5rem); + } + + .side-menu { + display: flex; flex-direction: row; + overflow-x: auto; + width: 100%; + white-space: nowrap; + padding: 0.5rem 1rem; } - .sidebar { - width: 250px; - height: 100vh; - position: sticky; - top: 0; - } - - .top-row { - position: sticky; - top: 0; - z-index: 1; - } - - .top-row.auth ::deep a:first-child { + .content { flex: 1; - text-align: right; - width: 0; - } - - .top-row, article { - padding-left: 2rem !important; - padding-right: 1.5rem !important; + padding: 1rem; } } diff --git a/Program/Layout/TopMenu.razor b/Program/Layout/TopMenu.razor new file mode 100644 index 0000000..1c788bc --- /dev/null +++ b/Program/Layout/TopMenu.razor @@ -0,0 +1 @@ +

TopMenu

\ No newline at end of file diff --git a/Program/Layout/TopMenu.razor.css b/Program/Layout/TopMenu.razor.css new file mode 100644 index 0000000..e69de29 diff --git a/wwwroot/index.html b/wwwroot/index.html index 10f3555..e1a6f39 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -7,7 +7,7 @@ - Front + AcaMate From 9dcf0db3036c807df7e9fde810c5edfb29675fda Mon Sep 17 00:00:00 2001 From: Seonkyu_Kim Date: Wed, 23 Apr 2025 17:50:00 +0900 Subject: [PATCH 09/17] =?UTF-8?q?[=E2=9C=A8]=20MainLayout=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EC=A4=91=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Program/Layout/MainLayout.razor | 28 ++++---- Program/Layout/MainLayout.razor.css | 105 ++++++++-------------------- wwwroot/css/app.css | 47 +++++++++++++ 3 files changed, 92 insertions(+), 88 deletions(-) diff --git a/Program/Layout/MainLayout.razor b/Program/Layout/MainLayout.razor index 1d07228..0064939 100644 --- a/Program/Layout/MainLayout.razor +++ b/Program/Layout/MainLayout.razor @@ -1,24 +1,26 @@ @inherits LayoutComponentBase +
- diff --git a/Program/Layout/MainLayout.razor.css b/Program/Layout/MainLayout.razor.css index 74ffa3a..162d7e9 100644 --- a/Program/Layout/MainLayout.razor.css +++ b/Program/Layout/MainLayout.razor.css @@ -1,93 +1,48 @@ +/* 전체 페이지 wrapper */ .page { - display: flex; - flex-direction: column; - height: 100vh; - overflow: hidden; + position: relative; + min-height: 100vh; + overflow-x: hidden; } -.top-row { +/* TopNav를 화면 위에 떠 있게 설정 */ +.top-nav { position: fixed; top: 0; left: 0; - right: 0; - height: 3.5rem; + width: 100%; + padding: 1.5rem 2rem; + background-color: rgba(255, 255, 255, 0.95); /* 반투명 배경 */ + z-index: 1000; display: flex; + justify-content: space-between; align-items: center; - background-color: #f7f7f7; - border-bottom: 1px solid #ddd; - z-index: 100; - padding: 0 1rem; + box-shadow: 0 2px 10px rgba(0,0,0,0.05); } -.top-title { - width: 250px; +/* 타이틀 텍스트 */ +.prj-title { + font-size: 1.5rem; font-weight: bold; + margin: 0; + color: var(--text-title); } +/* 메뉴 영역 */ .top-menu { - flex: 1; display: flex; - justify-content: flex-end; - gap: 1rem; + gap: 1.5rem; } -/* 본문 구조 */ -main { +.bottom-left-btn { + position: fixed; + z-index: 1000; + left: 40px; + width: 100%; + background-color: rgba(255, 255, 255, 0.95); /* 반투명 배경 */ + z-index: 1000; display: flex; - flex: 1; - margin-top: 3.5rem; - overflow: hidden; - /*height: calc(100vh - 3.5rem);*/ -} - -.side-menu { - width: 250px; - background: linear-gradient(180deg, rgb(5, 39, 103), #3a0647); - color: white; - padding: 1rem; - overflow-y: auto; - flex-shrink: 0; -} - -.content { - flex: 1; - padding: 1.5rem; - overflow-y: auto; - background-color: #fff; -} - -/* ✅ 모바일 대응 */ -@media (max-width: 768px) { - .top-row { - flex-direction: row; - justify-content: space-between; - } - - .top-title { - width: auto; - font-size: 1rem; - } - - .top-menu { - justify-content: flex-end; - } - - main { - flex-direction: column; - height: calc(100vh - 3.5rem); - } - - .side-menu { - display: flex; - flex-direction: row; - overflow-x: auto; - width: 100%; - white-space: nowrap; - padding: 0.5rem 1rem; - } - - .content { - flex: 1; - padding: 1rem; - } -} + justify-content: space-between; + align-items: center; + box-shadow: 0 2px 10px rgba(0,0,0,0.05); +} \ No newline at end of file diff --git a/wwwroot/css/app.css b/wwwroot/css/app.css index 54a8aa3..e7b8602 100644 --- a/wwwroot/css/app.css +++ b/wwwroot/css/app.css @@ -101,3 +101,50 @@ a, .btn-link { code { color: #c02d76; } + + +:root { + /* Disable */ + --disable-light: #F8F8F8; + --disable-normal: #B8B9B4; + --disable-dark: #8A8B87; + --disable-darker: #40413F; + + /* Information */ + --information-light: #F7FBF8; + --information-normal: #B2DBBB; + --information-dark: #86A48C; + --information-darker: #3E4D41; + + /* Point */ + --point-light: #F9FBFF; + --point-normal: #C2D6FA; + --point-dark: #92A1BC; + --point-darker: #444B58; + + /* Second */ + --second-light: #F2F0ED; + --second-normal: #79654E; + --second-dark: #5D4C3B; + --second-darker: #2A231B; + + /* Normal */ + --normal-light: #FDFCFB; + --normal-normal: #EBDFD2; + --normal-dark: #B0A79E; + --normal-darker: #524E4A; + + /* Danger */ + --danger-light: #FFF9F9; + --danger-normal: #FDC6C3; + --danger-dark: #BE9592; + --danger-darker: #594544; + + /* 기타 텍스트 및 상태 */ + --text-title: #1D1D1D; + --text-detail: #545454; + --text-disabled: #8E8E8E; + --text-white: #FFFFFF; + --text-black: #000000; + --text-border: #C6C6C6; +} From 7433b32b3b28c8644fabeffc97d154c889c443e3 Mon Sep 17 00:00:00 2001 From: Seonkyu_Kim Date: Thu, 24 Apr 2025 17:57:11 +0900 Subject: [PATCH 10/17] =?UTF-8?q?[=E2=9C=A8]=20=ED=8C=8C=EB=B9=84=EC=BD=98?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=A1=9C=EB=94=A9=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Program/Layout/MainLayout.razor | 44 ++++++-- Program/Layout/MainLayout.razor.css | 161 ++++++++++++++++++++++++++-- wwwroot/css/app.css | 150 ++++++++++++++++++++------ wwwroot/favicon.png | Bin 1148 -> 849 bytes wwwroot/index.html | 45 ++++++-- wwwroot/logo.png | Bin 0 -> 1393079 bytes 6 files changed, 343 insertions(+), 57 deletions(-) create mode 100644 wwwroot/logo.png diff --git a/Program/Layout/MainLayout.razor b/Program/Layout/MainLayout.razor index 0064939..ad55c02 100644 --- a/Program/Layout/MainLayout.razor +++ b/Program/Layout/MainLayout.razor @@ -1,23 +1,49 @@ @inherits LayoutComponentBase + +
-
-
-

PROJECT

+ @*
*@ + @*
*@ + @* icon *@ + @*

PROJECT

*@ + @*
*@ + @*
*@ + +
+ +
+ +
-
- @Body -
+ @*
*@ + @* @Body *@ + @* $1$ *@ +
+ @for (int i = 0; i < 50; i++) + { +
Dummy Box @i
+ } +
- -
- +
+ +
+
+
@*
*@ diff --git a/Program/Layout/MainLayout.razor.css b/Program/Layout/MainLayout.razor.css index 162d7e9..13ba7d2 100644 --- a/Program/Layout/MainLayout.razor.css +++ b/Program/Layout/MainLayout.razor.css @@ -1,3 +1,7 @@ +html, body { + overscroll-behavior: none; /* 바운스/오버스크롤 차단 */ +} + /* 전체 페이지 wrapper */ .page { position: relative; @@ -5,6 +9,11 @@ overflow-x: hidden; } +.top-view { + position: fixed; +} + + /* TopNav를 화면 위에 떠 있게 설정 */ .top-nav { position: fixed; @@ -20,6 +29,23 @@ box-shadow: 0 2px 10px rgba(0,0,0,0.05); } + +.logoIcon{ + width: 5vw; + min-width: 32px; + max-width: 128px; + height: auto; + aspect-ratio: 1/1; +} + + +/*.logo-mask img {*/ +/* visibility: hidden;*/ +/* width: 100%;*/ +/* height: 100%;*/ +/*}*/ + + /* 타이틀 텍스트 */ .prj-title { font-size: 1.5rem; @@ -34,15 +60,134 @@ gap: 1.5rem; } -.bottom-left-btn { +/* 테스트 플로팅 */ +.top-floating-menu { position: fixed; + top: 4rem; + right: 2rem; z-index: 1000; - left: 40px; - width: 100%; - background-color: rgba(255, 255, 255, 0.95); /* 반투명 배경 */ - z-index: 1000; + display: inline-block; +} + +/* 원형 버튼 (아래쪽 레이어) */ +.menu-toggle { + width: 48px; + height: 48px; + background-color: black; + color: white; + font-size: 1.5rem; + border-radius: 50%; display: flex; - justify-content: space-between; align-items: center; - box-shadow: 0 2px 10px rgba(0,0,0,0.05); -} \ No newline at end of file + justify-content: center; + cursor: pointer; + position: relative; + z-index: 1; /* 🔽 아래 레이어 */ +} + +/* 펼쳐질 박스 (위에 뜨는 레이어) */ +.menu-box { + position: absolute; + top: 0; + right: 0; + width: 85vw; + height: 120px; + background-color: white; + border-radius: 50%; + transform: scale(0); + transform-origin: top right; + opacity: 0; + transition: all 0.3s ease; + padding: 1rem; + pointer-events: none; + z-index: 2; /* 🔼 위쪽 레이어 */ + display: flex; + flex-direction: row; + gap: 0.5rem; +} + +/* 마우스를 전체에 올리면 박스가 앞에 뜨도록 */ +.top-floating-menu:hover .menu-box { + transform: scale(1); + border-radius: 0.75rem; + opacity: 1; + pointer-events: auto; +} +.menu-box { + position: absolute; + top: 0; + right: 0; + /* 나머지 스타일 유지 */ + padding-top: 2rem; /* X 버튼이 박스 내부에서 안 겹치도록 여백 추가 */ +} + +.menu-box button { + position: absolute; + top: 0.5rem; + right: 0.5rem; + background: transparent; + border: none; + font-size: 1.2rem; + color: #333; + cursor: pointer; + z-index: 3; +} + + + + + + + +/* 플로팅 뷰 */ +.float-left-down { + position: fixed; + left: 2rem; + bottom: 2rem; + width: clamp(48px, 8vw, 128px); + height: clamp(48px, 8vw, 128px); + z-index: 1000; +} + +.float-right-down { + position: fixed; + right: 2rem; + bottom: 2rem; + z-index: 1000; +} + +.fld-btn { + padding: 0.75rem 1.2rem; + background-color: var(--point-dark); + color: white; + border: none; + border-radius: 10px; + font-size: 1rem; + font-weight: bold; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); + cursor: pointer; + transition: all 0.2s ease; +} + +.fld-btn :hover { + transform: translateY(-3px); +} + + +/*더미 체크용*/ +.dummy-scroll-test { + overscroll-behavior: none; +} + +.dummy-box { + height: 150px; + background-color: var(--normal-normal); + margin: 1rem 0; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + border: 1px solid var(--normal-dark); +} + + diff --git a/wwwroot/css/app.css b/wwwroot/css/app.css index e7b8602..3cea11d 100644 --- a/wwwroot/css/app.css +++ b/wwwroot/css/app.css @@ -65,42 +65,67 @@ a, .btn-link { content: "An error has occurred." } -.loading-progress { - position: relative; - display: block; - width: 8rem; - height: 8rem; - margin: 20vh auto 1rem auto; + +/* 전체 화면을 덮는 로딩 오버레이 */ +.loading-overlay { + position: fixed; + z-index: 9999; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: #f0f0f0; + display: flex; + align-items: center; + justify-content: center; } - .loading-progress circle { - fill: none; - stroke: #e0e0e0; - stroke-width: 0.6rem; - transform-origin: 50% 50%; - transform: rotate(-90deg); - } - - .loading-progress circle:last-child { - stroke: #1b6ec2; - stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%; - transition: stroke-dasharray 0.05s ease-in-out; - } - -.loading-progress-text { - position: absolute; - text-align: center; - font-weight: bold; - inset: calc(20vh + 3.25rem) 0 auto 0.2rem; +/* 기존 코드 유지 */ +.custom-loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; } - .loading-progress-text:after { - content: var(--blazor-load-percentage-text, "Loading"); - } +/* 이후 동일한 스타일 유지 */ + +/*.loading-progress {*/ +/* position: relative;*/ +/* display: block;*/ +/* width: 8rem;*/ +/* height: 8rem;*/ +/* margin: 20vh auto 1rem auto;*/ +/*}*/ -code { - color: #c02d76; -} +/* .loading-progress circle {*/ +/* fill: none;*/ +/* stroke: #e0e0e0;*/ +/* stroke-width: 0.6rem;*/ +/* transform-origin: 50% 50%;*/ +/* transform: rotate(-90deg);*/ +/* }*/ + +/* .loading-progress circle:last-child {*/ +/* stroke: #1b6ec2;*/ +/* stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;*/ +/* transition: stroke-dasharray 0.05s ease-in-out;*/ +/* }*/ + +/*.loading-progress-text {*/ +/* position: absolute;*/ +/* text-align: center;*/ +/* font-weight: bold;*/ +/* inset: calc(20vh + 3.25rem) 0 auto 0.2rem;*/ +/*}*/ + +/* .loading-progress-text:after {*/ +/* content: var(--blazor-load-percentage-text, "Loading");*/ +/* }*/ + +/*code {*/ +/* color: #c02d76;*/ +/*}*/ :root { @@ -148,3 +173,66 @@ code { --text-black: #000000; --text-border: #C6C6C6; } + +/* Progress Icon */ + +.custom-loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + background: #f0f0f0; +} + +.logo-mask { + position: relative; + width: 192px; + height: 192px; + mask-image: url('/logo.png'); + mask-size: cover; + -webkit-mask-image: url('/logo.png'); + -webkit-mask-size: cover; +} + + +.logo-mask img { + visibility: hidden; + width: 100%; + height: 100%; +} + +.logo-progress { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 0%; + background-color: black; + animation: progressFill 3s linear forwards; +} + +@keyframes progressFill { + from { + width: 0%; + } + to { + width: 100%; + } +} + +.loading-text { + margin-top: 20px; + font-size: 1.2em; + color: #333; + animation: blink 1.2s infinite; +} + +@keyframes blink { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0; + } +} diff --git a/wwwroot/favicon.png b/wwwroot/favicon.png index 8422b59695935d180d11d5dbe99653e711097819..6c18ce7f7618c0cb17f97ac64f365498256f5c51 100644 GIT binary patch delta 827 zcmV-B1H}CN2+;}ak-}22vP@wiC@d=2`6r-OiUa}%Yy?FmNE*>b*5YN~?9R@+$70@O zv&+oxumLr&2X681pYxsb-TU49;0Q;!%MgIM|60UhkO8_Af`0&z=lODs@r{;jR>E*6 zfp?L_7{4pa(nV36ZUtlqtjhov78XvHWqDuQw@dge>M|hZdA^Kv!ggc(b-**MYw|o_ z4k7!-cJHtP##3JrWHtsp-R*WC1)3=w8PLqn&p%8Ep8=iWaJV^<^`NM&i-#mX*IEve z?rECl+!T%gG=GbWi%VISeIa-N+}NI2J+lZv(y9~+>^}H!unlLHmX;P7WnrAw3`o6R zuNV%S%aV>`8umFW`w37>z5uKOmh`Qp7XSr?-sa}!3Zt`-wXNy(dPP-LmnA)A-x9UI z!zsuB=Xw6Dq(=bR{&-_!<7?YL0SR=Dosw+2-R`|rb$|0Kg>X9IA8gh%%1v_^0SQ3T zg;DHuqps@@+p)KSAn{~bmM4Mjq9~qdn&yh6QzZB2LRgV}wcVYm6*Eo1*!>C6sn+4U zF~*mRqFe)hw9{`$q?BHZF`l)#D(N^F;|o<)T?g*p#~`r(y>8&Vq|EpaNxuUhArR`> zUcVQo+9*T1ReGIHM}xs&rVIq2i7`GX>G_cqe;z{ko;DRWec*lD zTZm+POGw7o>bkxNbbypnN+TcAlvq;*0IW*km~pcGK~+`1+I~jTMTzruUH=LGR44$M z?VEF1b_N)->$Vd>uszeIH$Ksw7~?NRQEbN;uYa^-d&^1PZg;sTil!(^#~9DH{c~+2 z&+}JXj;*379-m}-?-|H|VT|!bDLe)s@y>1H#?NX4sH&=KF~*<4>vdhf*YXX=yte0& z<^TqR!Nd&g6_5Z@O4mc+P221Je*cqpY&aoX057GK{%XI|?L1kQed&~9N@=s(-M+ED z4m2G6i2b{m_EQnq*{i+IC(VT&_tSV9{%`J&aD*cO`~#u=C|M{;s)PUl002ovPDHLk FV1mopnU??n delta 1128 zcmV-u1eg2K2K)$+BYy-#Nkl9h26h2-Cs%i*@Moc3?#6qJID|D#|3|2Hn z7gTIYEkr|%Xjp);YgvFmB&0#2E2b=|kVr)lMv9=KqwN&%obTp-$<51T%rx*NCwceh z-E+=&e(oLO`@Z~7gybJ#U|^tB2Pai}RN@5%1qsZ1e@R(XC4Z)}#hH$)EK*sna+7Na z!Jk&XUI!3KG?YjDAQh4sWtLh=_x_aAhd)U7>u)lzqEIaDlI;8{mydrGpzT_p%qlOCYz9Cn zXxm~vh)52&rGMVR%kDSdW#xvk0#JEl?Q0>x01eM*020K~7N%Fp&M)Sme*Gp~1voU_ z$2F0KON#_(J)eS6?wT>sA`%LEZL@V9ExmsJleD+>OFo!2cZB)3(XtT)of=u>u?mC+ zBoH$!B3oU>BS(1>nK8`unqCbG`_4`==Y<^QaVp=d@qc>x=)_>!S==FkTu62**xoy{fKhy8~E;*$~i|Y+C1xh>f>$JLl9F)UTqa+)UCt$63a>)Fb z1Ke=l_kV1i%dM-u0*u##7OxI7wwyCGpS?4UBFAr(%GBv5j$jS@@t@iI8?ZqE36I^4 zt+P^J9D^ELbS5KMtZ{Qn#JnSd$15nJ$ggkF%I4yUQC+BjDF z^}AtB7w348EL>7#sAsLWs}ndp8^DsAcOIL9TYny?aRN#Zo*Zf(T@nst^PcpB-Z%{~ z)ctLsOlxBkIK+eG1q~o=LSu?cLIN;1(AR@?PEXMz2>eGjgQ(R~MYt3XRC$c^o@*hy z8xQ9lUZ`uRbJRR _z{glespUbPj33%w+ha#GK#BP5+pCsh2#&CmT6vmg2m)lYql z4}Wh@Vr--?+UX~seZstv?zeqry=s1k-+J*a=lnDASJ+Q+*C9PBWw?7>8KRT8dXL~P zZ`KP4*x58$!!lfURCO`KS(0Fi6_XX{5t}sTEIE|BQh#3vCj>3bb|~Px4X8gj2?x3N zcvj^!5|r;hH-#|=dZ;`I&t~H(74&o}Vt;!@v%%{I2WM5fKpjB0JbGoS%+p-K9fD?M zF~+<#g5ayzF2ay7L&3zX-e!`E6@s^3#%kaj0ZE94#Q!W-pzBExtO8x^u`M|TWuOp1 z^MNsS%U&9fl8?U#;Z{kL-J`c5_Ml>*oUyN=@=OT6h@!SdhT&~<>>V$poRY3`O<~ye zI;j$6>~k@IMwD42=8$$!+R^@5o6HX(*n~
\ No newline at end of file diff --git a/Program/Views/Project/TopNav.razor.cs b/Program/Views/Project/TopNav.razor.cs index dc2e7b0..34c96a0 100644 --- a/Program/Views/Project/TopNav.razor.cs +++ b/Program/Views/Project/TopNav.razor.cs @@ -1,7 +1,17 @@ using Microsoft.AspNetCore.Components; -namespace Front.Program.Views.Company; +namespace Front.Program.Views.Project; public partial class TopNav : ComponentBase { + //로그인버튼을 누르면 페이지를 이동할거야 + [Inject] + NavigationManager NavigationManager { get; set; } = default!; + + public void OnClickLogin() + { + NavigationManager.NavigateTo("/auth"); + // Console.WriteLine("Redirecting to redirect page"); + } + } \ No newline at end of file diff --git a/_Imports.razor b/_Imports.razor index 90b8058..bf9db61 100644 --- a/_Imports.razor +++ b/_Imports.razor @@ -10,6 +10,6 @@ @using Front.Program.Layout @using Front.Program.Views.Project -@* @using Front.Program.Views.Academy *@ +@using Front.Program.Views.Academy diff --git a/wwwroot/css/tailwind.css b/wwwroot/css/tailwind.css index 162f5b1..5cc2da3 100644 --- a/wwwroot/css/tailwind.css +++ b/wwwroot/css/tailwind.css @@ -1 +1,940 @@ -*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.bottom-0{bottom:0}.left-0{left:0}.left-4{left:1rem}.right-4{right:1rem}.top-0{top:0}.top-1\/2{top:50%}.z-50{z-index:50}.mx-2{margin-right:.5rem}.ml-2,.mx-2{margin-left:.5rem}.ml-4{margin-left:1rem}.mr-0{margin-right:0}.mt-4{margin-top:1rem}.flex{display:flex}.hidden{display:none}.h-48{height:12rem}.h-8{height:2rem}.h-full{height:100%}.min-h-screen{min-height:100vh}.w-48{width:12rem}.w-8{width:2rem}.w-full{width:100%}.flex-1{flex:1 1 0%}.-translate-y-1\/2{--tw-translate-y:-50%}.-translate-y-1\/2,.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.overflow-hidden{overflow:hidden}.rounded{border-radius:.25rem}.bg-blue-600{--tw-bg-opacity:1;background-color:rgb(37 99 235/var(--tw-bg-opacity,1))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity,1))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity,1))}.bg-second-darker{--tw-bg-opacity:1;background-color:rgb(42 35 27/var(--tw-bg-opacity,1))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.bg-yellow-100{--tw-bg-opacity:1;background-color:rgb(254 249 195/var(--tw-bg-opacity,1))}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-4{padding-left:1rem;padding-right:1rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.pl-12{padding-left:3rem}.text-center{text-align:center}.font-sans{font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.text-lg{font-size:1.125rem;line-height:1.75rem}.font-bold{font-weight:700}.text-blue-600{--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity,1))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity,1))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity,1))}.text-normal-normal{--tw-text-opacity:1;color:rgb(235 223 210/var(--tw-text-opacity,1))}.text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity,1))}.text-red-800{--tw-text-opacity:1;color:rgb(153 27 27/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.underline{text-decoration-line:underline}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}@keyframes clipReveal{0%{clip-path:inset(0 100% 0 0)}to{clip-path:inset(0 0 0 0)}}.clip-reveal{animation:clipReveal 2.5s ease forwards}@keyframes blink{0%,to{opacity:1}50%{opacity:0}}.animate-blink{animation:blink 1.2s infinite}.mask-logo{-webkit-mask-image:url(/logo.png);-webkit-mask-size:cover;-webkit-mask-repeat:no-repeat;mask-image:url(/logo.png);mask-size:cover;mask-repeat:no-repeat;-webkit-mask-position:center;mask-position:center}.hover\:bg-blue-700:hover{--tw-bg-opacity:1;background-color:rgb(29 78 216/var(--tw-bg-opacity,1))}.hover\:text-blue-600:hover{--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity,1))} \ No newline at end of file +*, ::before, ::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; +} + +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; + --tw-contain-size: ; + --tw-contain-layout: ; + --tw-contain-paint: ; + --tw-contain-style: ; +} + +/* +! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com +*/ + +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: #e5e7eb; + /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +5. Use the user's configured `sans` font-feature-settings by default. +6. Use the user's configured `sans` font-variation-settings by default. +7. Disable tap highlights on iOS +*/ + +html, +:host { + line-height: 1.5; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -moz-tab-size: 4; + /* 3 */ + -o-tab-size: 4; + tab-size: 4; + /* 3 */ + font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + /* 4 */ + font-feature-settings: normal; + /* 5 */ + font-variation-settings: normal; + /* 6 */ + -webkit-tap-highlight-color: transparent; + /* 7 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; + /* 1 */ + line-height: inherit; + /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ + border-top-width: 1px; + /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font-family by default. +2. Use the user's configured `mono` font-feature-settings by default. +3. Use the user's configured `mono` font-variation-settings by default. +4. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + /* 1 */ + font-feature-settings: normal; + /* 2 */ + font-variation-settings: normal; + /* 3 */ + font-size: 1em; + /* 4 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ + border-collapse: collapse; + /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-feature-settings: inherit; + /* 1 */ + font-variation-settings: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + font-weight: inherit; + /* 1 */ + line-height: inherit; + /* 1 */ + letter-spacing: inherit; + /* 1 */ + color: inherit; + /* 1 */ + margin: 0; + /* 2 */ + padding: 0; + /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +input:where([type='button']), +input:where([type='reset']), +input:where([type='submit']) { + -webkit-appearance: button; + /* 1 */ + background-color: transparent; + /* 2 */ + background-image: none; + /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Reset default styling for dialogs. +*/ + +dialog { + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, textarea::-moz-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ + +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* Make elements with the HTML hidden attribute stay hidden by default */ + +[hidden]:where(:not([hidden="until-found"])) { + display: none; +} + +.fixed { + position: fixed; +} + +.absolute { + position: absolute; +} + +.relative { + position: relative; +} + +.inset-0 { + inset: 0px; +} + +.bottom-0 { + bottom: 0px; +} + +.left-0 { + left: 0px; +} + +.left-4 { + left: 1rem; +} + +.right-4 { + right: 1rem; +} + +.top-0 { + top: 0px; +} + +.top-1\/2 { + top: 50%; +} + +.z-50 { + z-index: 50; +} + +.mx-2 { + margin-left: 0.5rem; + margin-right: 0.5rem; +} + +.ml-2 { + margin-left: 0.5rem; +} + +.ml-4 { + margin-left: 1rem; +} + +.mr-0 { + margin-right: 0px; +} + +.mt-2 { + margin-top: 0.5rem; +} + +.mt-4 { + margin-top: 1rem; +} + +.mb-4 { + margin-bottom: 1rem; +} + +.flex { + display: flex; +} + +.hidden { + display: none; +} + +.h-48 { + height: 12rem; +} + +.h-8 { + height: 2rem; +} + +.h-full { + height: 100%; +} + +.min-h-screen { + min-height: 100vh; +} + +.w-48 { + width: 12rem; +} + +.w-8 { + width: 2rem; +} + +.w-full { + width: 100%; +} + +.max-w-xs { + max-width: 20rem; +} + +.flex-1 { + flex: 1 1 0%; +} + +.-translate-y-1\/2 { + --tw-translate-y: -50%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.transform { + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.flex-col { + flex-direction: column; +} + +.items-center { + align-items: center; +} + +.justify-end { + justify-content: flex-end; +} + +.justify-center { + justify-content: center; +} + +.overflow-hidden { + overflow: hidden; +} + +.rounded { + border-radius: 0.25rem; +} + +.border { + border-width: 1px; +} + +.border-gray-300 { + --tw-border-opacity: 1; + border-color: rgb(209 213 219 / var(--tw-border-opacity, 1)); +} + +.bg-blue-500 { + --tw-bg-opacity: 1; + background-color: rgb(59 130 246 / var(--tw-bg-opacity, 1)); +} + +.bg-blue-600 { + --tw-bg-opacity: 1; + background-color: rgb(37 99 235 / var(--tw-bg-opacity, 1)); +} + +.bg-blue-700 { + --tw-bg-opacity: 1; + background-color: rgb(29 78 216 / var(--tw-bg-opacity, 1)); +} + +.bg-gray-100 { + --tw-bg-opacity: 1; + background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1)); +} + +.bg-gray-50 { + --tw-bg-opacity: 1; + background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1)); +} + +.bg-second-darker { + --tw-bg-opacity: 1; + background-color: rgb(42 35 27 / var(--tw-bg-opacity, 1)); +} + +.bg-white { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1)); +} + +.bg-yellow-100 { + --tw-bg-opacity: 1; + background-color: rgb(254 249 195 / var(--tw-bg-opacity, 1)); +} + +.bg-red-600 { + --tw-bg-opacity: 1; + background-color: rgb(220 38 38 / var(--tw-bg-opacity, 1)); +} + +.p-4 { + padding: 1rem; +} + +.p-6 { + padding: 1.5rem; +} + +.p-2 { + padding: 0.5rem; +} + +.px-4 { + padding-left: 1rem; + padding-right: 1rem; +} + +.py-2 { + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + +.pl-12 { + padding-left: 3rem; +} + +.text-center { + text-align: center; +} + +.font-sans { + font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; +} + +.text-2xl { + font-size: 1.5rem; + line-height: 2rem; +} + +.text-lg { + font-size: 1.125rem; + line-height: 1.75rem; +} + +.font-bold { + font-weight: 700; +} + +.text-blue-600 { + --tw-text-opacity: 1; + color: rgb(37 99 235 / var(--tw-text-opacity, 1)); +} + +.text-gray-700 { + --tw-text-opacity: 1; + color: rgb(55 65 81 / var(--tw-text-opacity, 1)); +} + +.text-gray-900 { + --tw-text-opacity: 1; + color: rgb(17 24 39 / var(--tw-text-opacity, 1)); +} + +.text-normal-normal { + --tw-text-opacity: 1; + color: rgb(235 223 210 / var(--tw-text-opacity, 1)); +} + +.text-red-600 { + --tw-text-opacity: 1; + color: rgb(220 38 38 / var(--tw-text-opacity, 1)); +} + +.text-red-800 { + --tw-text-opacity: 1; + color: rgb(153 27 27 / var(--tw-text-opacity, 1)); +} + +.text-white { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity, 1)); +} + +.underline { + text-decoration-line: underline; +} + +.shadow { + --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +/*@import './components/';*/ + +/* 클리핑 애니메이션 (왼쪽부터 점점 보임) */ + +@keyframes clipReveal { + from { + clip-path: inset(0 100% 0 0); + /* 왼쪽만 남기고 나머지 잘라냄 */ + } + + to { + clip-path: inset(0 0 0 0); + /* 전체 다 보임 */ + } +} + +.clip-reveal { + animation: clipReveal 2500ms ease forwards; +} + +/* 텍스트 블링크 */ + +@keyframes blink { + 0%, 100% { + opacity: 1; + } + + 50% { + opacity: 0; + } +} + +.animate-blink { + animation: blink 1.2s infinite; +} + +/* 로고 마스크 설정 */ + +.mask-logo { + -webkit-mask-image: url('/logo.png'); + -webkit-mask-size: cover; + -webkit-mask-repeat: no-repeat; + mask-image: url('/logo.png'); + mask-size: cover; + mask-repeat: no-repeat; + -webkit-mask-position: center; + mask-position: center; +} + +.hover\:bg-blue-700:hover { + --tw-bg-opacity: 1; + background-color: rgb(29 78 216 / var(--tw-bg-opacity, 1)); +} + +.hover\:bg-blue-800:hover { + --tw-bg-opacity: 1; + background-color: rgb(30 64 175 / var(--tw-bg-opacity, 1)); +} + +.hover\:text-blue-600:hover { + --tw-text-opacity: 1; + color: rgb(37 99 235 / var(--tw-text-opacity, 1)); +} + +@media (min-width: 640px) { + .sm\:p-6 { + padding: 1.5rem; + } +} + +@media (min-width: 768px) { + .md\:block { + display: block; + } + + .md\:w-64 { + width: 16rem; + } + + .md\:flex-row { + flex-direction: row; + } + + .md\:p-8 { + padding: 2rem; + } +}