[] FRONT 반영 머지
All checks were successful
AcaMate_FO/pipeline/head This commit looks good

This commit is contained in:
김선규 2025-05-16 13:59:40 +09:00
commit a35f4a9229
38 changed files with 1578 additions and 460 deletions

13
.gitignore vendored
View File

@ -1,10 +1,20 @@
# 특정 환경에 따라 추가
/private/
/publish/
/bin/
/obj/
./private/
./privacy/
./publish/
publish/
./bin/
# 노드 관련
node_modules/
package-lock.json
# 기본 파일 및 폴더 제외
*.log
@ -14,6 +24,7 @@ publish/
*.swp
# macOS 관련 파일 제외
._
._*
.DS_Store
.AppleDouble

View File

@ -1,8 +1,31 @@
<Router AppAssembly="@typeof(App).Assembly">
<!--
Router: Blazor의 라우팅 기능을 제공하는 컴포넌트
AppAssembly: 현재 애플리케이션의 어셈블리를 지정
Blazor는 @page가 붙은 .razor 파일을 찾아서 App 어셈블리 안에서 자동으로 라우팅을 찾아 설정
-->
<Router AppAssembly="@typeof(App).Assembly">
@* <!-- *@
@* Found: URL에 해당하는 페이지가 존재해 라우팅이 성공적으로 이루어졌을 때 실행 *@
@* Context="routeData": Found 컴포넌트에 전달된 해당하는 URL 페이지의 라우팅 정보를 routeData 라는 이름으로 사용 *@
@* RouteView: 실제로 해당 URL 에 해당하는 .razor 페이지를 렌더링 *@
@* RouteData="@routeData": Found 에서 전달된 라우팅 결과 *@
@* DefaultLayout="@typeof(MainLayout)": 해당 페이지에 @layout 이 없다면 사용 할 기본 레이아웃을 지정 *@
@* FocusOnNavigate: URL이 변경되었을 때 페이지의 특정 요소에 포커스를 맞추는 컴포넌트 - 접근성이나 키보드 탐색을 위한 UX 개선 요소 *@
@* RouteData="@routeData": Found 에서 전달된 라우팅 결과 *@
@* Selector="h1": 포커스를 맞출 요소를 지정 *@
@* --> *@
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
<FocusOnNavigate RouteData="@routeData" Selector="h1"/>
</Found>
<!--
NotFound: URL에 해당하는 페이지가 존재하지 않을 때 실행
LayoutView: 레이아웃을 지정하는 컴포넌트
Layout="@typeof(MainLayout)": 레이아웃을 지정
role="alert": 스크린 리더와 같은 보조 기술에 의해 읽히는 경고 메시지
-->
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">

View File

@ -0,0 +1,227 @@
# 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
- 데이터를 정의하는 역할
- ./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를 통해 처리한다.

View File

@ -13,5 +13,22 @@
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.8" PrivateAssets="all"/>
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="Program\Layout\MainLayout.razor" />
</ItemGroup>
<ItemGroup>
<_ContentIncludedByDefault Remove="Pages\Counter.razor" />
<_ContentIncludedByDefault Remove="Pages\Home.razor" />
<_ContentIncludedByDefault Remove="Pages\Weather.razor" />
<_ContentIncludedByDefault Remove="wwwroot\css\bootstrap\bootstrap.min.css" />
<_ContentIncludedByDefault Remove="wwwroot\css\bootstrap\bootstrap.min.css.map" />
</ItemGroup>
<ItemGroup>
<Folder Include="Program\Models\" />
<Folder Include="Program\ViewModels\" />
</ItemGroup>
</Project>

View File

@ -1,16 +0,0 @@
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
</div>
<article class="content px-4">
@Body
</article>
</main>
</div>

View File

@ -1,77 +0,0 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.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 {
flex: 1;
text-align: right;
width: 0;
}
.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}

View File

@ -1,39 +0,0 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">Front</a>
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</div>
<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="weather">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
</NavLink>
</div>
</nav>
</div>
@code {
private bool collapseNavMenu = true;
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
private void ToggleNavMenu()
{
collapseNavMenu = !collapseNavMenu;
}
}

View File

@ -1,83 +0,0 @@
.navbar-toggler {
background-color: rgba(255, 255, 255, 0.1);
}
.top-row {
height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep a {
color: #d7d7d7;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep a:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.collapse {
/* Never collapse the sidebar for wide screens */
display: block;
}
.nav-scrollable {
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}

View File

@ -1,18 +0,0 @@
@page "/counter"
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}

View File

@ -1,7 +0,0 @@
@page "/"
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.

View File

@ -1,57 +0,0 @@
@page "/weather"
@inject HttpClient Http
<PageTitle>Weather</PageTitle>
<h1>Weather</h1>
<p>This component demonstrates fetching data from the server.</p>
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
private WeatherForecast[]? forecasts;
protected override async Task OnInitializedAsync()
{
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json");
}
public class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}

View File

@ -0,0 +1,50 @@
@* @inherits LayoutComponentBase *@
@* *@
@* *@
@* <div class="min-h-screen bg-gray-50 text-gray-900"> *@
@* <TopBanner /> *@
@* <TopNav /> *@
@* *@
@* <div class="flex flex-1"> *@
@* <SideNav /> *@
@* *@
@* <main class="flex-1 p-6"> *@
@* $1$ <!-- Body는 URL 뒤에 입력할 페이지에 따라서 그거에 맞는 @page를 찾아서 열어준다. --> #1# *@
@* @Body *@
@* </main> *@
@* </div> *@
@* *@
@* <FloatingButton /> *@
@* <BottomNav /> *@
@* <Footer/> *@
@* </div> *@
@inherits LayoutComponentBase
<div class="min-h-screen flex flex-col bg-gray-50 text-gray-900">
<!-- Top 영역 -->
<TopBanner />
<TopNav />
<!-- 본문 영역 -->
<div class="flex flex-1 flex-col md:flex-row">
@* <!-- 사이드 메뉴 --> *@
@* <div class="hidden md:block md:w-64 bg-white shadow"> *@
@* <SideNav /> *@
@* </div> *@
<!-- 본문 컨텐츠 -->
<main class="flex-1 p-4 sm:p-6 md:p-8">
@Body
</main>
</div>
<!-- 플로팅 버튼 -->
<FloatingButton />
<!-- 하단 메뉴 -->
<BottomNav />
<Footer />
</div>

View File

@ -0,0 +1 @@
<h3>TopMenu</h3>

View File

@ -0,0 +1,10 @@
@page "/auth"
<div class="flex flex-col items-center justify-center min-h-screen bg-gray-100">
<h1 class="text-2xl font-bold mb-4">로그인</h1>
<div class="w-full max-w-xs">
<input type="text" placeholder="아이디" class="mb-4 p-2 border border-gray-300 rounded w-full" />
<input type="password" placeholder="비밀번호" class="mb-4 p-2 border border-gray-300 rounded w-full" />
<button class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 w-full">로그인</button>
</div>
</div>

View File

@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Components;
namespace Front.Program.Views.Academy;
public partial class Auth : ComponentBase
{
}

View File

@ -0,0 +1,16 @@
@page "/about"
<div class="bg-blue-500 text-white p-4">
<h2 class="text-2xl font-bold">About Us</h2>
<p class="mt-2">We are a team of passionate developers.</p>
<p>이건 그냥 일반 텍스트야</p>
@* 클릭시 onClick 이벤트 발생 *@
<button class="bg-blue-700 text-white px-4 py-2 rounded hover:bg-blue-800"
@onclick="OnClickEvent">
Click Me
</button>
</div>

View File

@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Components;
namespace Front.Program.Views.Project;
public partial class About : ComponentBase
{
[Inject]
NavigationManager NavigationManager { get; set; } = default!;
private void OnClickEvent()
{
// NavigationManager.NavigateTo("/redirectpage");
Console.WriteLine("Redirecting to redirect page");
}
}

View File

@ -0,0 +1 @@
@page "/"

View File

@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Components;
namespace Front.Program.Views.Project;
public partial class RedirectPage : ComponentBase
{
[Inject]
private NavigationManager NavigationManager { get; set; } = default!;
protected override void OnInitialized()
{
NavigationManager.NavigateTo("/about",true);
}
}

View File

@ -0,0 +1,18 @@
@* <div class="flex items-center p-4 bg-second-darker shadow"> *@
@* $1$ 이동하는 거 말고 그냥 일반 텍스트 넣을거야 #1# *@
@* <img src="/logo.png" alt="Icon" class="w-8 h-8 mr-0 absolute left-4 top-1/2 transform -translate-y-1/2"> *@
@* <h3 class="flex-1 text-center text-normal-normal pl-12">일반 텍스트 작성</h3> *@
@* </div> *@
<div class="relative bg-second-darker shadow p-4 flex items-center justify-center">
<!-- 왼쪽 아이콘 -->
<div class="absolute left-4 flex items-center">
<img src="/logo.png" alt="Icon" class="w-8 h-8">
</div>
<!-- 가운데 텍스트 -->
<h3 class="text-normal-normal text-center">긴급 공지 안내문입니다.</h3>
<!-- 오른쪽 가짜 공간 (아이콘 크기만큼) -->
<div class="absolute right-4 w-8 h-8"></div>
</div>

View File

@ -0,0 +1,7 @@
using Microsoft.AspNetCore.Components;
namespace Front.Program.Views.Project;
public partial class TopBanner : ComponentBase
{
}

View File

@ -0,0 +1,11 @@
<div class="flex justify-end items-center p-4 bg-white shadow">
<a href="#about" class="mx-2 text-gray-700 hover:text-blue-600">About</a>
<a href="#join" class="mx-2 text-gray-700 hover:text-blue-600">Join</a>
<a href="#notice" class="mx-2 text-gray-700 hover:text-blue-600">Whats New</a>
<button class="ml-4 px-4 py-2 bg-red-600 text-white rounded hover:bg-blue-700"
@onclick="OnClickLogin">
Login
</button>
</div>

View File

@ -0,0 +1,17 @@
using Microsoft.AspNetCore.Components;
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");
}
}

View File

@ -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"
}

View File

@ -1,8 +1,22 @@
# Web Client
## Project Rule
### [[📚 PROJECT RULE (클릭시 이동)]](Documents/Project%20RULE%2010.md)
***필수 사항은 반드시 지켜주세요!***
---
## Development Environment
### Skill
- Blazor WebAssembly
- HTML & CSS
### IDE
- JetBrains Rider
<<<<<<< HEAD
=======
---
>>>>>>> seonkyu.kim-main

View File

@ -7,4 +7,9 @@
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using Front
@using Front.Layout
@using Front.Program.Layout
@using Front.Program.Views.Project
@using Front.Program.Views.Academy

13
package.json Normal file
View File

@ -0,0 +1,13 @@
{
"name": "tailwind-blazor-template",
"version": "1.0.0",
"scripts": {
"build:css": "tailwindcss -i ./wwwroot/css/app.css -o ./wwwroot/css/tailwind.css --minify",
"watch:css": "tailwindcss -i ./wwwroot/css/app.css -o ./wwwroot/css/tailwind.css --watch"
},
"devDependencies": {
"tailwindcss": "^3.4.1",
"postcss": "^8.4.21",
"autoprefixer": "^10.4.14"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

66
tailwind.config.js Normal file
View File

@ -0,0 +1,66 @@
module.exports = {
content: [
"./**/*.razor",
"./wwwroot/index.html"
],
theme: {
extend: {
colors: {
disable: {
light: '#F8F8F8',
normal: '#B8B9B4',
dark: '#8A8B87',
darker: '#40413F'
},
information: {
light: '#F7FBF8',
normal: '#B2DBBB',
dark: '#86A48C',
darker: '#3E4D41'
},
point: {
light: '#F9FBFF',
normal: '#C2D6FA',
dark: '#92A1BC',
darker: '#444B58'
},
second: {
light: '#F2F0ED',
normal: '#79654E',
dark: '#5D4C3B',
darker: '#2A231B'
},
normal: {
light: '#FDFCFB',
normal: '#EBDFD2',
dark: '#B0A79E',
darker: '#524E4A'
},
danger: {
light: '#FFF9F9',
normal: '#FDC6C3',
dark: '#BE9592',
darker: '#594544'
},
text: {
title: '#1D1D1D',
detail: '#545454',
disabled: '#8E8E8E',
white: '#FFFFFF',
black: '#000000',
border: '#C6C6C6'
}
},
fontFamily: {},
screens: {
'xs': '400px',
'sm': '640px',
'md': '768px',
'lg': '1024px',
'xl': '1280px',
'2xl': '1536px'
}
},
},
plugins: [],
}

View File

@ -1,103 +1,42 @@
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
@tailwind base;
@tailwind components;
@tailwind utilities;
h1:focus {
outline: none;
}
/*@import './components/';*/
a, .btn-link {
color: #0071c1;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}
.content {
padding-top: 1.1rem;
}
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
.invalid {
outline: 1px solid red;
}
.validation-message {
color: red;
}
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
/* 클리핑 애니메이션 (왼쪽부터 점점 보임) */
@keyframes clipReveal {
from {
clip-path: inset(0 100% 0 0); /* 왼쪽만 남기고 나머지 잘라냄 */
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
to {
clip-path: inset(0 0 0 0); /* 전체 다 보임 */
}
.loading-progress {
position: relative;
display: block;
width: 8rem;
height: 8rem;
margin: 20vh auto 1rem auto;
}
.loading-progress circle {
fill: none;
stroke: #e0e0e0;
stroke-width: 0.6rem;
transform-origin: 50% 50%;
transform: rotate(-90deg);
.clip-reveal {
animation: clipReveal 2500ms ease forwards;
}
/* 텍스트 블링크 */
@keyframes blink {
0%, 100% {
opacity: 1;
}
.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");
50% {
opacity: 0;
}
code {
color: #c02d76;
}
.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;
mask-position: center;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

940
wwwroot/css/tailwind.css Normal file
View File

@ -0,0 +1,940 @@
*, ::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;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 849 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -1,32 +1,53 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Front</title>
<base href="/" />
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="css/app.css" />
<link rel="icon" type="image/png" href="favicon.png" />
<link href="Front.styles.css" rel="stylesheet" />
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AcaMate</title>
<base href="/" />
<link rel="stylesheet" href="css/tailwind.css" />
<link rel="stylesheet" href="css/app.css" />
<link rel="icon" type="image/png" href="favicon.png" />
</head>
<body class="bg-gray-50 text-gray-900 font-sans">
<body>
<div id="app">
<svg class="loading-progress">
<circle r="40%" cx="50%" cy="50%" />
<circle r="40%" cx="50%" cy="50%" />
</svg>
<div class="loading-progress-text"></div>
<!-- 로딩 오버레이 -->
<div class="fixed inset-0 z-50 flex items-center justify-center bg-gray-100" id="loading-overlay">
<!-- 로딩 오버레이 내부 -->
<div class="flex flex-col items-center">
<div class="relative w-48 h-48 overflow-hidden">
<img src="/logo.png" alt="logo"
class="absolute top-0 left-0 w-full h-full clip-reveal" />
</div>
<div class="mt-4 text-lg text-gray-700 animate-blink">Loading...</div>
</div>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.webassembly.js"></script>
<div id="app"></div>
<!-- Blazor 에러 UI -->
<div id="blazor-error-ui" class="hidden fixed bottom-0 left-0 w-full bg-yellow-100 text-red-800 p-4 shadow z-50">
An unhandled error has occurred.
<a href="" class="underline text-blue-600 ml-2">Reload</a>
<button class="ml-4 text-red-600 font-bold dismiss"></button>
</div>
<!-- Blazor WASM 로딩 스크립트 -->
<script src="_framework/blazor.webassembly.js" autostart="false"></script>
<script>
const MIN_LOADING_MS = 2700;
const loadingStart = performance.now();
Blazor.start().then(() => {
const elapsed = performance.now() - loadingStart;
const remaining = Math.max(0, MIN_LOADING_MS - elapsed);
setTimeout(() => {
const overlay = document.getElementById('loading-overlay');
if (overlay) overlay.remove();
}, remaining);
}).catch(err => {
console.error("❌ Blazor 로딩 실패", err);
});
</script>
</body>
</html>

BIN
wwwroot/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@ -1,27 +0,0 @@
[
{
"date": "2022-01-06",
"temperatureC": 1,
"summary": "Freezing"
},
{
"date": "2022-01-07",
"temperatureC": 14,
"summary": "Bracing"
},
{
"date": "2022-01-08",
"temperatureC": -13,
"summary": "Freezing"
},
{
"date": "2022-01-09",
"temperatureC": -16,
"summary": "Balmy"
},
{
"date": "2022-01-10",
"temperatureC": -2,
"summary": "Chilly"
}
]