init source
This commit is contained in:
276
pages/about.vue
Normal file
276
pages/about.vue
Normal file
@@ -0,0 +1,276 @@
|
||||
<template>
|
||||
<div class="about">
|
||||
<section class="hero">
|
||||
<h1>About Us</h1>
|
||||
<p class="subtitle">혁신적인 솔루션으로 미래를 만들어갑니다</p>
|
||||
</section>
|
||||
|
||||
<section class="mission">
|
||||
<h2>Our Mission</h2>
|
||||
<p>
|
||||
우리는 최신 기술을 활용하여 사용자 중심의 혁신적인 제품을 개발하고, 더
|
||||
나은 디지털 경험을 제공하는 것을 목표로 합니다.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="values">
|
||||
<h2>Our Values</h2>
|
||||
<div class="values-grid">
|
||||
<div class="value-card">
|
||||
<h3>혁신</h3>
|
||||
<p>끊임없는 혁신을 통해 새로운 가치를 창출합니다</p>
|
||||
</div>
|
||||
<div class="value-card">
|
||||
<h3>품질</h3>
|
||||
<p>최고의 품질을 위해 세심한 주의를 기울입니다</p>
|
||||
</div>
|
||||
<div class="value-card">
|
||||
<h3>협력</h3>
|
||||
<p>팀워크와 협력을 통해 더 큰 성과를 달성합니다</p>
|
||||
</div>
|
||||
<div class="value-card">
|
||||
<h3>성장</h3>
|
||||
<p>지속적인 학습과 성장을 추구합니다</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="team">
|
||||
<h2>Our Team</h2>
|
||||
<div class="team-grid">
|
||||
<div class="team-member">
|
||||
<div class="member-avatar">
|
||||
<span>👨💻</span>
|
||||
</div>
|
||||
<h3>김개발</h3>
|
||||
<p class="position">Frontend Developer</p>
|
||||
<p>
|
||||
Vue.js와 Nuxt.js 전문가로 사용자 경험에 중점을 둔 개발을 담당합니다.
|
||||
</p>
|
||||
</div>
|
||||
<div class="team-member">
|
||||
<div class="member-avatar">
|
||||
<span>👩💻</span>
|
||||
</div>
|
||||
<h3>이디자인</h3>
|
||||
<p class="position">UI/UX Designer</p>
|
||||
<p>사용자 중심의 직관적이고 아름다운 인터페이스를 설계합니다.</p>
|
||||
</div>
|
||||
<div class="team-member">
|
||||
<div class="member-avatar">
|
||||
<span>👨🔧</span>
|
||||
</div>
|
||||
<h3>박백엔드</h3>
|
||||
<p class="position">Backend Developer</p>
|
||||
<p>안정적이고 확장 가능한 서버 아키텍처를 구축합니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="contact">
|
||||
<h2>Contact Us</h2>
|
||||
<p>궁금한 점이 있으시면 언제든 연락주세요!</p>
|
||||
<div class="contact-info">
|
||||
<p>📧 Email: contact@example.com</p>
|
||||
<p>📱 Phone: 02-1234-5678</p>
|
||||
<p>📍 Address: 서울특별시 강남구 테헤란로 123</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 페이지 메타데이터 설정
|
||||
definePageMeta({
|
||||
title: "About",
|
||||
description: "우리 팀과 미션에 대해 알아보세요",
|
||||
});
|
||||
|
||||
// SEO 최적화
|
||||
useHead({
|
||||
title: "About Us - Nuxt.js App",
|
||||
meta: [
|
||||
{
|
||||
name: "description",
|
||||
content: "혁신적인 솔루션으로 미래를 만들어가는 우리 팀을 소개합니다.",
|
||||
},
|
||||
{ property: "og:title", content: "About Us - Nuxt.js App" },
|
||||
{
|
||||
property: "og:description",
|
||||
content: "혁신적인 솔루션으로 미래를 만들어가는 우리 팀을 소개합니다.",
|
||||
},
|
||||
],
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.about {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.hero {
|
||||
text-align: center;
|
||||
padding: 3rem 0;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.2rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
section {
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #333;
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mission {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.mission p {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.6;
|
||||
color: #666;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.values-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 2rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.value-card {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.value-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.value-card h3 {
|
||||
color: #00dc82;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.value-card p {
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.team-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.team-member {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.team-member:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.member-avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: linear-gradient(135deg, #00dc82, #00b894);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 1rem;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.team-member h3 {
|
||||
color: #333;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.position {
|
||||
color: #00dc82;
|
||||
font-weight: bold;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.team-member p:last-child {
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.contact {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.contact p {
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.contact-info {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.contact-info p {
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.hero h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.values-grid,
|
||||
.team-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.hero,
|
||||
.mission,
|
||||
.contact {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
173
pages/admin/codes.vue
Normal file
173
pages/admin/codes.vue
Normal file
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<div class="codes-page">
|
||||
<div class="page-header">
|
||||
<h1>공통코드</h1>
|
||||
<div class="page-actions">
|
||||
<button class="action-btn">코드 추가</button>
|
||||
<button class="action-btn">그룹코드 추가</button>
|
||||
<button class="action-btn primary">저장</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="aggrid-section">
|
||||
<div class="aggrid-title">그룹코드</div>
|
||||
<div class="aggrid-container">
|
||||
<ag-grid-vue
|
||||
class="ag-theme-material"
|
||||
style="width: 100%; height: 180px"
|
||||
:column-defs="groupColDefs"
|
||||
:row-data="groupRowData"
|
||||
:default-col-def="defaultColDef"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="aggrid-section">
|
||||
<div class="aggrid-title">코드</div>
|
||||
<div class="aggrid-container">
|
||||
<ag-grid-vue
|
||||
class="ag-theme-material"
|
||||
style="width: 100%; height: 320px"
|
||||
:column-defs="codeColDefs"
|
||||
:row-data="codeRowData"
|
||||
:default-col-def="defaultColDef"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { AgGridVue } from "ag-grid-vue3";
|
||||
import { ModuleRegistry, AllCommunityModule } from "ag-grid-community";
|
||||
import type { GroupCode, Code, ColDef, DefaultColDef } from "~/types/ag-grid";
|
||||
|
||||
// AG Grid 모듈 등록
|
||||
onMounted(() => {
|
||||
ModuleRegistry.registerModules([AllCommunityModule]);
|
||||
});
|
||||
|
||||
const groupColDefs = ref<ColDef[]>([
|
||||
{ headerName: "그룹 코드", field: "groupCode", width: 140 },
|
||||
{ headerName: "그룹 코드명", field: "groupName", width: 180 },
|
||||
{ headerName: "사용 여부", field: "useYn", width: 100 },
|
||||
{ headerName: "정렬 순서", field: "order", width: 100 },
|
||||
]);
|
||||
|
||||
const groupRowData = ref<GroupCode[]>([
|
||||
{
|
||||
groupCode: "MONITORING_001",
|
||||
groupName: "모니터링 기본정보",
|
||||
useYn: "Y",
|
||||
order: 1,
|
||||
},
|
||||
{ groupCode: "RESOURCE_001", groupName: "리소스 유형", useYn: "Y", order: 6 },
|
||||
]);
|
||||
|
||||
const codeColDefs = ref<ColDef[]>([
|
||||
{ headerName: "코드", field: "code", width: 160 },
|
||||
{ headerName: "코드명", field: "codeName", width: 120 },
|
||||
{ headerName: "코드 상세", field: "codeDetail", width: 200 },
|
||||
{ headerName: "부모 코드", field: "parentCode", width: 120 },
|
||||
{ headerName: "사용 여부", field: "useYn", width: 100 },
|
||||
{ headerName: "정렬 순서", field: "order", width: 100 },
|
||||
]);
|
||||
|
||||
const codeRowData = ref<Code[]>([
|
||||
{
|
||||
code: "RESOURCE_001_001",
|
||||
codeName: "facility",
|
||||
codeDetail: "주요 시설을 나타냄",
|
||||
parentCode: "",
|
||||
useYn: "Y",
|
||||
order: 1,
|
||||
},
|
||||
{
|
||||
code: "RESOURCE_001_002",
|
||||
codeName: "equipment",
|
||||
codeDetail: "사업에 설치된 장비",
|
||||
parentCode: "",
|
||||
useYn: "Y",
|
||||
order: 2,
|
||||
},
|
||||
{
|
||||
code: "RESOURCE_001_003",
|
||||
codeName: "device",
|
||||
codeDetail: "IoT 디바이스 또는 센서",
|
||||
parentCode: "",
|
||||
useYn: "Y",
|
||||
order: 3,
|
||||
},
|
||||
{
|
||||
code: "RESOURCE_001_004",
|
||||
codeName: "station",
|
||||
codeDetail: "데이터 수집 또는 처리 스테이션",
|
||||
parentCode: "",
|
||||
useYn: "Y",
|
||||
order: 4,
|
||||
},
|
||||
]);
|
||||
|
||||
const defaultColDef = ref<DefaultColDef>({
|
||||
resizable: true,
|
||||
sortable: true,
|
||||
filter: true,
|
||||
minWidth: 80,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.codes-page {
|
||||
padding: 32px 32px 0 32px;
|
||||
}
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.page-header h1 {
|
||||
font-size: 1.35rem;
|
||||
font-weight: 700;
|
||||
color: #222;
|
||||
margin: 0;
|
||||
}
|
||||
.page-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
.action-btn {
|
||||
background: #f4f6fa;
|
||||
border: 1px solid #e0e7ef;
|
||||
color: #1976d2;
|
||||
font-weight: 500;
|
||||
border-radius: 5px;
|
||||
padding: 7px 18px;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
.action-btn.primary {
|
||||
background: #1976d2;
|
||||
color: #fff;
|
||||
border: none;
|
||||
}
|
||||
.action-btn:hover {
|
||||
background: #e6f0fa;
|
||||
}
|
||||
.aggrid-section {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.aggrid-title {
|
||||
font-size: 1.08rem;
|
||||
font-weight: 600;
|
||||
color: #1976d2;
|
||||
margin-bottom: 8px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
.aggrid-container {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.04);
|
||||
padding: 18px 18px 0 18px;
|
||||
}
|
||||
</style>
|
118
pages/admin/logs.vue
Normal file
118
pages/admin/logs.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<div class="logs-page">
|
||||
<div class="page-header">
|
||||
<div class="breadcrumb">관리자 메뉴 / 접속기록</div>
|
||||
</div>
|
||||
<div class="filter-section">
|
||||
<label for="date-input">날짜 설정</label>
|
||||
<input id="date-input" v-model="selectedDate" type="date" />
|
||||
</div>
|
||||
<div class="aggrid-section">
|
||||
<div class="aggrid-container">
|
||||
<ag-grid-vue
|
||||
class="ag-theme-material"
|
||||
style="width: 100%; height: 220px"
|
||||
:column-defs="colDefs"
|
||||
:row-data="filteredRows"
|
||||
:default-col-def="defaultColDef"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="copyright">
|
||||
© 2024. Ocean Monitoring System. All Rights Reserved.
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { AgGridVue } from "ag-grid-vue3";
|
||||
import { ModuleRegistry, AllCommunityModule } from "ag-grid-community";
|
||||
import type { LogEntry, ColDef, DefaultColDef } from "~/types/ag-grid";
|
||||
|
||||
onMounted(() => {
|
||||
ModuleRegistry.registerModules([AllCommunityModule]);
|
||||
});
|
||||
|
||||
const selectedDate = ref<string>("2025-06-27");
|
||||
const rows = ref<LogEntry[]>([
|
||||
{ account: "admin3", datetime: "Jun 27, 2025, 9:51:04 AM", ip: "172.17.0.1" },
|
||||
{ account: "admin9", datetime: "Jun 27, 2025, 8:51:41 AM", ip: "172.17.0.1" },
|
||||
]);
|
||||
|
||||
const colDefs = ref<ColDef[]>([
|
||||
{ headerName: "계정", field: "account", width: 160 },
|
||||
{ headerName: "접속 일시", field: "datetime", width: 200 },
|
||||
{ headerName: "IP", field: "ip", width: 160 },
|
||||
]);
|
||||
|
||||
const defaultColDef = ref<DefaultColDef>({
|
||||
resizable: true,
|
||||
sortable: true,
|
||||
filter: true,
|
||||
minWidth: 80,
|
||||
});
|
||||
|
||||
const filteredRows = computed<LogEntry[]>(() => {
|
||||
// 실제 구현시 날짜 필터링 적용
|
||||
return rows.value;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.logs-page {
|
||||
padding: 32px 32px 0 32px;
|
||||
}
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.breadcrumb {
|
||||
font-size: 1.08rem;
|
||||
color: #1976d2;
|
||||
font-weight: 500;
|
||||
}
|
||||
.filter-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 18px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
.filter-section label {
|
||||
font-size: 1rem;
|
||||
color: #222;
|
||||
font-weight: 500;
|
||||
}
|
||||
.filter-section input[type="date"] {
|
||||
border: 1px solid #e0e7ef;
|
||||
border-radius: 5px;
|
||||
padding: 6px 12px;
|
||||
font-size: 1rem;
|
||||
color: #222;
|
||||
background: #f4f6fa;
|
||||
}
|
||||
.aggrid-section {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.aggrid-container {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.04);
|
||||
padding: 18px 18px 0 18px;
|
||||
}
|
||||
.table-info {
|
||||
font-size: 0.98rem;
|
||||
color: #666;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.copyright {
|
||||
text-align: right;
|
||||
color: #888;
|
||||
font-size: 0.98rem;
|
||||
margin: 32px 8px 0 0;
|
||||
padding-bottom: 18px;
|
||||
}
|
||||
</style>
|
148
pages/admin/programs.vue
Normal file
148
pages/admin/programs.vue
Normal file
@@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<div class="codes-page">
|
||||
<div class="page-header">
|
||||
<h1>프로그램 관리</h1>
|
||||
<div class="page-actions">
|
||||
<button class="action-btn">프로그램 추가</button>
|
||||
<button class="action-btn primary">저장</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="aggrid-section">
|
||||
<div class="aggrid-title">프로그램 목록</div>
|
||||
<div class="aggrid-container">
|
||||
<ag-grid-vue
|
||||
class="ag-theme-material"
|
||||
style="width: 100%; height: 600px"
|
||||
:column-defs="programColDefs"
|
||||
:row-data="programRowData"
|
||||
:default-col-def="defaultColDef"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { AgGridVue } from "ag-grid-vue3";
|
||||
import { ModuleRegistry, AllCommunityModule } from "ag-grid-community";
|
||||
import type { Program, ColDef, DefaultColDef } from "~/types/ag-grid";
|
||||
|
||||
onMounted(() => {
|
||||
ModuleRegistry.registerModules([AllCommunityModule]);
|
||||
});
|
||||
|
||||
const programColDefs = ref<ColDef[]>([
|
||||
{ headerName: "부모 코드", field: "parentCode", width: 100 },
|
||||
{ headerName: "레벨", field: "level", width: 60 },
|
||||
{ headerName: "코드", field: "code", width: 120 },
|
||||
{ headerName: "이름", field: "name", width: 160 },
|
||||
{ headerName: "사용 여부", field: "useYn", width: 80 },
|
||||
{ headerName: "메뉴 여부", field: "menuYn", width: 80 },
|
||||
{ headerName: "API 여부", field: "apiYn", width: 80 },
|
||||
{ headerName: "예외 허용 여부", field: "exceptionYn", width: 110 },
|
||||
{ headerName: "표시 순서", field: "order", width: 80 },
|
||||
{ headerName: "uri", field: "uri", width: 180 },
|
||||
{ headerName: "필드1", field: "field1", width: 100 },
|
||||
{ headerName: "필드2", field: "field2", width: 100 },
|
||||
{ headerName: "필드3", field: "field3", width: 100 },
|
||||
]);
|
||||
|
||||
const programRowData = ref<Program[]>([
|
||||
{
|
||||
parentCode: "OMS01",
|
||||
level: 1,
|
||||
code: "OMS01_01",
|
||||
name: "DASHBOARD",
|
||||
useYn: true,
|
||||
menuYn: true,
|
||||
apiYn: false,
|
||||
exceptionYn: false,
|
||||
order: 1,
|
||||
uri: "/dashboard/overview",
|
||||
field1: "ri-pie-chart-2-line",
|
||||
field2: "",
|
||||
field3: "",
|
||||
},
|
||||
{
|
||||
parentCode: "OMS01_02",
|
||||
level: 2,
|
||||
code: "OMS01_02_01",
|
||||
name: "IoT 센서이력 조회",
|
||||
useYn: true,
|
||||
menuYn: true,
|
||||
apiYn: false,
|
||||
exceptionYn: false,
|
||||
order: 5,
|
||||
uri: "/environmental/iot-sensor-history",
|
||||
field1: "ri-line-chart-line",
|
||||
field2: "",
|
||||
field3: "",
|
||||
},
|
||||
// ... (샘플 데이터 추가)
|
||||
]);
|
||||
|
||||
const defaultColDef = ref<DefaultColDef>({
|
||||
resizable: true,
|
||||
sortable: true,
|
||||
filter: true,
|
||||
minWidth: 80,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.codes-page {
|
||||
padding: 32px 32px 0 32px;
|
||||
}
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.page-header h1 {
|
||||
font-size: 1.35rem;
|
||||
font-weight: 700;
|
||||
color: #222;
|
||||
margin: 0;
|
||||
}
|
||||
.page-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
.action-btn {
|
||||
background: #f4f6fa;
|
||||
border: 1px solid #e0e7ef;
|
||||
color: #1976d2;
|
||||
font-weight: 500;
|
||||
border-radius: 5px;
|
||||
padding: 7px 18px;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
.action-btn.primary {
|
||||
background: #1976d2;
|
||||
color: #fff;
|
||||
border: none;
|
||||
}
|
||||
.action-btn:hover {
|
||||
background: #e6f0fa;
|
||||
}
|
||||
.aggrid-section {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.aggrid-title {
|
||||
font-size: 1.08rem;
|
||||
font-weight: 600;
|
||||
color: #1976d2;
|
||||
margin-bottom: 8px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
.aggrid-container {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.04);
|
||||
padding: 18px 18px 0 18px;
|
||||
}
|
||||
</style>
|
123
pages/index.vue
Normal file
123
pages/index.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<div
|
||||
class="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 py-12 px-4"
|
||||
>
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-4xl font-bold text-gray-900 mb-4">
|
||||
Integrated Bio Foundry Platform
|
||||
</h1>
|
||||
<p class="text-xl text-gray-600">
|
||||
통합 바이오 파운드리 플랫폼에 오신 것을 환영합니다
|
||||
</p>
|
||||
|
||||
<!-- 사용자 환영 메시지 -->
|
||||
<div
|
||||
v-if="userStore.isLoggedIn"
|
||||
class="mt-6 p-4 bg-white rounded-lg shadow-md inline-block"
|
||||
>
|
||||
<p class="text-lg text-gray-800 mb-2">
|
||||
안녕하세요,
|
||||
<span class="font-semibold text-blue-600">{{
|
||||
userStore.userName
|
||||
}}</span
|
||||
>님!
|
||||
</p>
|
||||
<p class="text-sm text-gray-600">
|
||||
{{ userStore.isAdmin ? "관리자" : "사용자" }} 권한으로
|
||||
로그인되었습니다.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="mt-6 p-4 bg-yellow-50 rounded-lg shadow-md inline-block"
|
||||
>
|
||||
<p class="text-lg text-gray-800 mb-2">로그인이 필요합니다</p>
|
||||
<NuxtLink
|
||||
to="/login"
|
||||
class="inline-block mt-2 bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-4 rounded-lg transition-colors"
|
||||
>
|
||||
로그인하기
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tailwind CSS 테스트 섹션 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
<div
|
||||
class="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow"
|
||||
>
|
||||
<div
|
||||
class="w-12 h-12 bg-blue-500 rounded-full flex items-center justify-center mb-4 mx-auto"
|
||||
>
|
||||
<span class="text-white font-bold">1</span>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-2">Feature 1</h3>
|
||||
<p class="text-gray-600">Tailwind CSS가 정상 작동하고 있습니다!</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow"
|
||||
>
|
||||
<div
|
||||
class="w-12 h-12 bg-green-500 rounded-full flex items-center justify-center mb-4 mx-auto"
|
||||
>
|
||||
<span class="text-white font-bold">2</span>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-2">Feature 2</h3>
|
||||
<p class="text-gray-600">반응형 디자인이 적용되었습니다.</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow"
|
||||
>
|
||||
<div
|
||||
class="w-12 h-12 bg-purple-500 rounded-full flex items-center justify-center mb-4 mx-auto"
|
||||
>
|
||||
<span class="text-white font-bold">3</span>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-2">Feature 3</h3>
|
||||
<p class="text-gray-600">모던한 UI 컴포넌트를 사용할 수 있습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 버튼 테스트 -->
|
||||
<div class="text-center space-x-4">
|
||||
<button
|
||||
class="bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-4 rounded-lg transition-colors"
|
||||
>
|
||||
Primary Button
|
||||
</button>
|
||||
<button
|
||||
class="bg-gray-500 hover:bg-gray-600 text-white font-medium py-2 px-4 rounded-lg transition-colors"
|
||||
>
|
||||
Secondary Button
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useUserStore } from "~/stores/user";
|
||||
|
||||
// 페이지 메타데이터 설정
|
||||
definePageMeta({
|
||||
title: "Home",
|
||||
description: "Welcome to our Nuxt.js application",
|
||||
});
|
||||
|
||||
const userStore = useUserStore();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #00dc82;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
223
pages/login.vue
Normal file
223
pages/login.vue
Normal file
@@ -0,0 +1,223 @@
|
||||
<template>
|
||||
<div class="login-bg">
|
||||
<div class="login-card">
|
||||
<h1 class="login-title">Integrated Bio Foundry Platform</h1>
|
||||
<div class="login-form">
|
||||
<h2 class="login-signin">Sign In</h2>
|
||||
|
||||
<!-- 에러 메시지 -->
|
||||
<div v-if="errorMessage" class="error-message">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
|
||||
<label class="login-label" for="userId">ID</label>
|
||||
<input
|
||||
id="userId"
|
||||
v-model="userId"
|
||||
class="login-input"
|
||||
type="text"
|
||||
placeholder="아이디를 입력하세요"
|
||||
:disabled="isLoading"
|
||||
/>
|
||||
<label class="login-label" for="password">Password</label>
|
||||
<input
|
||||
id="password"
|
||||
v-model="password"
|
||||
class="login-input"
|
||||
type="password"
|
||||
placeholder="비밀번호를 입력하세요"
|
||||
:disabled="isLoading"
|
||||
@keyup.enter="signIn"
|
||||
/>
|
||||
<button class="login-btn" :disabled="isLoading" @click="signIn">
|
||||
<span v-if="isLoading">로그인 중...</span>
|
||||
<span v-else>SIGN IN</span>
|
||||
</button>
|
||||
|
||||
<!-- 테스트 계정 안내 -->
|
||||
<div class="test-accounts">
|
||||
<p class="test-title">테스트 계정:</p>
|
||||
<p class="test-account">관리자: admin / stam1201!</p>
|
||||
<p class="test-account">일반 사용자: user / stam1201!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useUserStore } from "~/stores/user";
|
||||
|
||||
// auth 레이아웃 사용
|
||||
definePageMeta({
|
||||
layout: "auth",
|
||||
});
|
||||
|
||||
const userId = ref("");
|
||||
const password = ref("");
|
||||
const errorMessage = ref("");
|
||||
const isLoading = ref(false);
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 이미 로그인된 경우 홈으로 리다이렉션
|
||||
onMounted(() => {
|
||||
if (userStore.isLoggedIn) {
|
||||
router.push("/");
|
||||
}
|
||||
});
|
||||
|
||||
async function signIn() {
|
||||
if (!userId.value || !password.value) {
|
||||
errorMessage.value = "아이디와 비밀번호를 입력해주세요.";
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
errorMessage.value = "";
|
||||
|
||||
try {
|
||||
const result = await userStore.login(userId.value, password.value);
|
||||
|
||||
if (result.success) {
|
||||
// 로그인 성공 시 홈으로 이동
|
||||
await router.push("/");
|
||||
} else {
|
||||
errorMessage.value = result.error || "로그인에 실패했습니다.";
|
||||
}
|
||||
} catch (error) {
|
||||
errorMessage.value = "로그인 중 오류가 발생했습니다.";
|
||||
console.error("로그인 오류:", error);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-bg {
|
||||
width: 100vw;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f8f9fb;
|
||||
padding: 0;
|
||||
}
|
||||
.login-card {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 16px 40px 0 rgba(44, 62, 80, 0.08);
|
||||
padding: 40px 36px 32px 36px;
|
||||
min-width: 500px;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.login-title {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 24px;
|
||||
color: #23272f;
|
||||
font-family: "Montserrat", "Pretendard", sans-serif;
|
||||
}
|
||||
.login-form {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.login-signin {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 16px;
|
||||
color: #23272f;
|
||||
}
|
||||
.login-label {
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 4px;
|
||||
color: #6b7280;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.login-input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
background: #f1f5fb;
|
||||
margin-bottom: 4px;
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
transition: border 0.2s;
|
||||
}
|
||||
.login-input:focus {
|
||||
border: 1.5px solid #4666e5;
|
||||
}
|
||||
.login-input:disabled {
|
||||
background: #f3f4f6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
padding: 10px 0;
|
||||
background: #4666e5;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.login-btn:hover:not(:disabled) {
|
||||
background: #3451b2;
|
||||
}
|
||||
.login-btn:disabled {
|
||||
background: #9ca3af;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.error-message {
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
color: #dc2626;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.test-accounts {
|
||||
margin-top: 24px;
|
||||
padding: 16px;
|
||||
background: #f8fafc;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
.test-title {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
.test-account {
|
||||
font-size: 0.8rem;
|
||||
color: #64748b;
|
||||
margin: 4px 0;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.login-card {
|
||||
padding: 24px 8px 20px 8px;
|
||||
min-width: 0;
|
||||
max-width: 98vw;
|
||||
}
|
||||
.login-title {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user