첫 커밋

This commit is contained in:
eld_master 2024-12-06 14:24:22 +09:00
commit 1e374d9973
71 changed files with 6534 additions and 0 deletions

View File

@ -0,0 +1,3 @@
3799CE17CA15679658F7CAE69B19A8F1EFB13A7E2F138F035CE2F6ECCBD65963
sectigo.com
dcv20240722dfb99

1
allscore/.env Normal file
View File

@ -0,0 +1 @@
VITE_APP_API_URL=http://localhost:5001/

View File

@ -0,0 +1 @@
VITE_APP_API_URL=http://localhost:5000/

33
allscore/.eslintrc.cjs Normal file
View File

@ -0,0 +1,33 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-prettier'
],
env: {
'vue/setup-compiler-macros': true
},
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
},
rules: {
'prettier/prettier': [
'error',
{
singleQuote: true,
// semi: true
// tabWidth: 4,
// trailingComma: 'all',
printWidth: 80,
bracketSpacing: true,
arrowParens: 'avoid',
endOfLine: 'auto' // 한줄 추가
}
]
}
}

30
allscore/.gitignore vendored Normal file
View File

@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

View File

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"tabWidth": 2,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "none"
}

7
allscore/.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,7 @@
{
"recommendations": [
"Vue.volar",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}

35
allscore/README.md Normal file
View File

@ -0,0 +1,35 @@
# vue3-posts
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Compile and Minify for Production
```sh
npm run build
```
### Lint with [ESLint](https://eslint.org/)
```sh
npm run lint
```

42
allscore/components.d.ts vendored Normal file
View File

@ -0,0 +1,42 @@
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
AboutView: typeof import('./src/views/AboutView.vue')['default']
App: typeof import('./src/App.vue')['default']
AppAlert: typeof import('./src/components/app/AppAlert.vue')['default']
AppCard: typeof import('./src/components/app/AppCard.vue')['default']
AppError: typeof import('./src/components/app/AppError.vue')['default']
AppGrid: typeof import('./src/components/app/AppGrid.vue')['default']
AppLoading: typeof import('./src/components/app/AppLoading.vue')['default']
AppModal: typeof import('./src/components/app/AppModal.vue')['default']
AppPagination: typeof import('./src/components/app/AppPagination.vue')['default']
Card: typeof import('./src/card/Card.vue')['default']
CardTest: typeof import('./src/card/CardTest.vue')['default']
copy: typeof import('./src/card/Card copy.vue')['default']
HomeView: typeof import('./src/views/HomeView.vue')['default']
MyPage: typeof import('./src/views/MyPage.vue')['default']
NestedHomeView: typeof import('./src/views/nested/NestedHomeView.vue')['default']
NestedOneView: typeof import('./src/views/nested/NestedOneView.vue')['default']
NestedTwoView: typeof import('./src/views/nested/NestedTwoView.vue')['default']
NestedView: typeof import('./src/views/nested/NestedView.vue')['default']
NotFoundView: typeof import('./src/views/NotFoundView.vue')['default']
PostCreateView: typeof import('./src/views/posts/PostCreateView.vue')['default']
PostDetailView: typeof import('./src/views/posts/PostDetailView.vue')['default']
PostEditView: typeof import('./src/views/posts/PostEditView.vue')['default']
PostFilter: typeof import('./src/components/posts/PostFilter.vue')['default']
PostForm: typeof import('./src/components/posts/PostForm.vue')['default']
PostItem: typeof import('./src/components/posts/PostItem.vue')['default']
PostListView: typeof import('./src/views/posts/PostListView.vue')['default']
PostModal: typeof import('./src/components/posts/PostModal.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
TheHeader: typeof import('./src/layouts/TheHeader.vue')['default']
TheView: typeof import('./src/layouts/TheView.vue')['default']
}
}

52
allscore/db.json Normal file
View File

@ -0,0 +1,52 @@
{
"posts": [
{
"title": "asdf11@@1",
"content": "123ㅋ",
"createdAt": 1717335471487,
"id": 13
},
{
"title": "제목1",
"content": "내용1\n1",
"createdAt": 1717336912826,
"id": 14
},
{
"title": "제목111",
"content": "내용111",
"createdAt": 1717339828635,
"id": 15
},
{
"title": "123",
"content": "12311ㅁ",
"createdAt": 1717585818053,
"id": 16
},
{
"title": "ㅁㄴㅇㄻㄴㅇㄹ1",
"content": "ㅁㄴㅇㄻㄴㅇㄹ2",
"createdAt": 1717586219791,
"id": 17
},
{
"title": "asdfasdf",
"content": "asdfasdfasdf",
"createdAt": 1717591306939,
"id": 18
},
{
"title": "111111111111",
"content": "1222222",
"createdAt": 1717591390087,
"id": 20
},
{
"title": "운동 나무 위키운동 나무 위키운동 나무 위키운동 나무 위키",
"content": "운동 나무 위키운동 나무 위키운동 나무 위키운동 나무 위키",
"createdAt": 1718100857725,
"id": 21
}
]
}

14
allscore/index.html Normal file
View File

@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<div id="modal"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

10
allscore/jsconfig.json Normal file
View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"baseUrl": ".",
"jsx": "preserve",
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

4247
allscore/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

34
allscore/package.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "vue3-posts",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --port 3000",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore",
"format": "prettier --write src/",
"db": "json-server --watch db.json --port 5000"
},
"dependencies": {
"axios": "^1.7.2",
"bootstrap": "^5.3.3",
"bootstrap-icons": "^1.11.3",
"dayjs": "^1.11.11",
"pinia": "^2.1.7",
"vue": "^3.4.21",
"vue-router": "^4.3.2"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.8.0",
"@vitejs/plugin-vue": "^5.0.4",
"@vue/eslint-config-prettier": "^9.0.0",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.23.0",
"json-server": "^0.17.4",
"prettier": "^3.2.5",
"unplugin-vue-components": "^0.27.0",
"vite": "^5.2.8"
}
}

BIN
allscore/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

28
allscore/src/App.vue Normal file
View File

@ -0,0 +1,28 @@
<!-- <script setup>
import TheHeader from '@/layouts/TheHeader.vue'
import TheView from '@/layouts/TheView.vue'
</script>
<template>
<TheHeader></TheHeader>
<TheView></TheView>
<AppAlert />
</template>
<style scoped></style> -->
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style>
/* App.vue에 대한 스타일을 추가할 수 있습니다 */
</style>

10
allscore/src/api/index.js Normal file
View File

@ -0,0 +1,10 @@
import axios from 'axios'
function create(baseURL, options) {
const instance = axios.create(Object.assign({ baseURL }, options))
return instance
}
export const posts = create(`${import.meta.env.VITE_APP_API_URL}posts/`)
// development : http://localhost:5000/posts/
// production : http://localhost:5001/posts/

25
allscore/src/api/posts.js Normal file
View File

@ -0,0 +1,25 @@
import { posts } from '.'
export function getPosts(params) {
return posts.get('/', { params })
}
export function getPostById(id) {
return posts.get(`/${id}`)
}
export function createPost(data) {
return posts.post('/', data)
}
export function updatePost(id, data) {
return posts.patch(`/${id}`, data)
}
// export function updatePost(id, data) {
// return posts.put(`/${id}`, data)
// }
export function deletePost(id) {
return posts.delete(`/${id}`)
}

View File

@ -0,0 +1,86 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

View File

@ -0,0 +1,35 @@
@import './base.css';
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
padding: 3px;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
@media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
#app {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 2rem;
}
}

150
allscore/src/card/Card.vue Normal file
View File

@ -0,0 +1,150 @@
<template>
<div class="container">
<h1>법인카드1</h1>
<h1>사용장부</h1>
<div class="form-group">
<label for="year">연도</label>
<select id="year" v-model="selectedYear" class="form-control">
<option v-for="year in years" :key="year" :value="year">
{{ year }}
</option>
</select>
</div>
<div class="form-group">
<label for="month"></label>
<select id="month" v-model="selectedMonth" class="form-control">
<option v-for="month in months" :key="month" :value="month">
{{ month }}
</option>
</select>
</div>
<button @click="downloadSummary" class="btn btn-primary">통계 다운로드</button>
<hr />
<div class="form-group">
<label>사용자</label>
<div v-for="user in users" :key="user.id" class="form-check">
<input
type="checkbox"
class="form-check-input"
:id="user.id"
:value="user.id"
v-model="selectedUsers"
/>
<label :for="user.id" class="form-check-label">{{ user.name }}</label>
</div>
</div>
<div class="form-group">
<label for="amount">사용 금액</label>
<input type="number" id="amount" v-model="form.amount" class="form-control" />
</div>
<div class="form-group">
<label for="purpose">사용 목적</label>
<input type="text" id="purpose" v-model="form.purpose" class="form-control" />
</div>
<button @click="submitForm" class="btn btn-success">제출하기</button>
</div>
</template>
<script>
import axios from 'axios'
const axiosInstance = axios.create({
baseURL: 'https://eldsoft.com:8097/api/card'
})
export default {
data() {
return {
form: {
amount: '',
purpose: ''
},
selectedUsers: [],
selectedYear: new Date().getFullYear(),
selectedMonth: new Date().getMonth() + 1,
years: Array.from({ length: 10 }, (v, i) => new Date().getFullYear() - i),
months: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
users: [
{ id: 'jang', name: '장진아' },
{ id: 'yeo', name: '여정훈' },
{ id: 'kim', name: '김지수' },
{ id: 'choi', name: '최재혁' }
]
}
},
methods: {
async downloadSummary() {
const payload_excel = {
year: this.selectedYear,
month: this.selectedMonth
}
console.log('Download Payload:', payload_excel)
try {
const response = await axiosInstance.post('/excel', payload_excel, {
responseType: 'blob'
})
if (response.data.size === 0) {
alert('해당월에 데이터가 없습니다')
return
}
const url = window.URL.createObjectURL(
new Blob([response.data], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
)
const link = document.createElement('a')
link.href = url
link.setAttribute('download', 'summary.xlsx')
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
} catch (error) {
console.error('There was an error downloading the file!', error)
alert('An error occurred while downloading the file.')
}
},
async submitForm() {
const payload_regist = {
jang: this.selectedUsers.includes('jang') ? 'Y' : 'N',
yeo: this.selectedUsers.includes('yeo') ? 'Y' : 'N',
kim: this.selectedUsers.includes('kim') ? 'Y' : 'N',
choi: this.selectedUsers.includes('choi') ? 'Y' : 'N',
amount: this.form.amount,
purpose: this.form.purpose
}
console.log('Request Payload:', payload_regist)
try {
const response = await axiosInstance.post('/regist', payload_regist)
alert('Registration successful')
console.log('Response Data:', response.data)
} catch (error) {
console.error('There was an error!', error)
alert('An error occurred: Server error')
}
}
}
}
</script>
<style scoped>
.container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.form-group {
margin-bottom: 15px;
}
.btn {
margin-top: 20px;
}
hr {
margin: 20px 0;
}
</style>

View File

@ -0,0 +1,115 @@
<template>
<div class="container">
<nav class="navbar">
<div class="logo">
<img src="@/assets/logo.svg" alt="Logo" />
</div>
<ul class="nav-links">
<li><a href="#">공지사항</a></li>
</ul>
<div class="auth-buttons">
<button @click="login">로그인</button>
</div>
</nav>
<div class="content">
<div class="main-title">법인카드 사용장부</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'App',
methods: {
login() {
console.log('로그인 버튼 클릭됨')
}
}
}
</script>
<style scoped>
.navbar {
position: fixed;
top: 0;
left: 0;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
background-color: #333;
color: #fff;
}
.logo img {
height: 40px;
}
.nav-links {
list-style: none;
align-items: center;
display: flex;
height: 100%; /* 부모 높이만큼 높이 설정 */
}
.nav-links li {
margin-right: 20px;
display: flex;
align-items: center; /* 수직 중앙 정렬 */
height: 100%; /* 부모 높이만큼 높이 설정 */
}
.nav-links a {
color: #fff;
text-decoration: none;
}
.auth-buttons {
display: flex;
align-items: center; /* 수직 중앙 정렬 */
}
.auth-buttons button {
background-color: #ff6600;
color: #fff;
border: none;
padding: 10px 20px;
cursor: pointer;
}
.auth-buttons button:hover {
background-color: #e65c00;
}
html,
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
}
body {
display: flex;
justify-content: center; /* 수평 중앙 정렬 */
align-items: flex-start; /* 상단 정렬 */
}
.container {
margin-top: 70px;
width: 1000px; /* 원하는 고정 너비 */
text-align: center; /* 내부 콘텐츠의 수평 중앙 정렬 */
}
.content {
width: 1000px;
text-align: center;
}
.main-title {
top: 0;
}
</style>

View File

@ -0,0 +1,44 @@
<template>
<div class="app-alert">
<TransitionGroup name="slide">
<div
v-for="({ message, type }, index) in alerts"
:key="index"
class="alert"
:class="typeStyle(type)"
role="alert"
>
{{ message }}
</div>
</TransitionGroup>
</div>
</template>
<script setup>
import { useAlert } from '@/composables/alert'
const { alerts } = useAlert()
const typeStyle = (type) => (type === 'error' ? 'alert-danger' : 'alert-primary')
</script>
<style scoped>
.app-alert {
position: fixed;
top: 10px;
right: 10px;
}
.slide-enter-from,
.slide-leave-to {
opacity: 0;
transform: translateY(-30px);
}
.slide-enter-active,
.vslide-leave-active {
transition: all 0.5s ease;
}
.slide-enter-to,
.slide-leave-from {
transform: translateY(0px);
opacity: 1;
}
</style>

View File

@ -0,0 +1,13 @@
<template>
<div class="card">
<div v-if="$slots.header" class="card-header">
<slot name="header"></slot>
</div>
<div v-if="$slots.default" class="card-body">
<slot></slot>
</div>
<div v-if="$slots.footer" class="card-footer">
<slot name="footer"></slot>
</div>
</div>
</template>

View File

@ -0,0 +1,13 @@
<template>
<div class="text-center text-danger py-4">
{{ message }}
</div>
</template>
<script setup>
defineProps({
message: String
})
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,22 @@
<template>
<div class="row g-3">
<div v-for="(item, index) in items" :key="index" :class="colClass">
<slot :item="item" :index="index"></slot>
</div>
</div>
</template>
<script setup>
defineProps({
items: {
type: Array,
required: true
},
colClass: {
type: String,
default: 'col-4'
}
})
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,7 @@
<template>
<div class="d-flex justify-content-center">
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</template>

View File

@ -0,0 +1,61 @@
<template>
<Transition>
<div v-if="modelValue">
<div class="modal-backdrop fade show"></div>
<div
class="modal fade show d-block"
tabindex="-1"
aria-labelledby="exampleModalLabel"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<slot name="header">
<h1 class="modal-title fs-5" id="exampleModalLabel">
{{ title }}
</h1>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
@click="$emit('update:modelValue', false)"
></button>
</slot>
</div>
<div class="modal-body">
<slot></slot>
</div>
<div class="modal-footer">
<slot name="actions"> </slot>
</div>
</div>
</div>
</div>
</div>
</Transition>
</template>
<script setup>
defineProps({
modelValue: Boolean,
title: String
})
defineEmits(['close', 'update:modelValue'])
</script>
<style scoped>
.v-enter-from,
.v-leave-to {
opacity: 0;
}
.v-enter-active,
.v-leave-active {
transition: all 0.5s ease;
}
.v-enter-to,
.v-leave-from {
opacity: 1;
}
</style>

View File

@ -0,0 +1,58 @@
<template>
<nav class="mt-5" aria-label="Page navigation example">
<ul class="pagination justify-content-center">
<li class="page-item" :class="isPrevPage">
<a
class="page-link"
href="#"
aria-label="Previous"
@click.prevent="$emit('page', currentPage - 1)"
>
<span aria-hidden="true">&laquo;</span>
</a>
</li>
<li
v-for="page in pageCount"
:key="page"
class="page-item"
:class="{ active: currentPage === page }"
>
<a class="page-link" href="#" @click.prevent="$emit('page', page)">{{
page
}}</a>
</li>
<li class="page-item" :class="isNextPage">
<a
class="page-link"
href="#"
aria-label="Next"
@click.prevent="$emit('page', currentPage + 1)"
>
<span aria-hidden="true">&raquo;</span>
</a>
</li>
</ul>
</nav>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
currentPage: {
type: Number,
required: true
},
pageCount: {
type: Number,
required: true
}
})
defineEmits(['page'])
const isPrevPage = computed(() => ({ disabled: !(props.currentPage > 1) }))
const isNextPage = computed(() => ({
disabled: !(props.currentPage < props.pageCount)
}))
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,41 @@
<template>
<form @submit.prevent>
<div class="row g-3">
<div class="col">
<input
:value="title"
@input="changeTitle"
type="text"
class="form-control"
placeholder="제목으로 검색해주세요."
/>
</div>
<div class="col-4">
<select
:value="limit"
@input="$emit('update:limit', $event.target.value)"
class="form-select"
>
<option value="6">6개씩 보기</option>
<option value="12">12개씩 보기</option>
<option value="18">18개씩 보기</option>
</select>
</div>
</div>
</form>
</template>
<script setup>
defineProps({
title: String,
limit: Number
})
const emits = defineEmits(['update:title', 'update:limit'])
const changeTitle = (event) => {
setTimeout(() => {
emits('update:title', event.target.value)
}, 500)
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,45 @@
<template>
<form>
<div class="mb-3">
<label for="title" class="form-label">제목</label>
<input
v-focus
v-color="'red'"
:value="title"
@input="$emit('update:title', $event.target.value)"
type="text"
class="form-control"
id="title"
/>
</div>
<div class="mb-3">
<label for="contents" class="form-label">내용</label>
<textarea
:value="content"
@input="$emit('update:content', $event.target.value)"
class="form-control"
id="contents"
rows="3"
></textarea>
</div>
<div class="d-flex gap-2 mt-4">
<slot name="actions"></slot>
</div>
</form>
</template>
<script setup>
const vFocus = {
mounted: el => {
el.focus()
}
}
defineProps({
title: String,
content: String
})
defineEmits(['update:title', 'update:content'])
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,41 @@
<template>
<AppCard>
<h5 class="card-title text-truncate">{{ title }}</h5>
<p class="card-text text-truncate">
{{ content }}
</p>
<p class="text-muted">{{ createdDate }}</p>
<template #footer>
<div class="d-flex flex-row-reverse">
<button class="btn p-1" @click.stop="$emit('modal')">
<i class="bi bi-emoji-sunglasses"></i>
</button>
<button class="btn p-1" @click.stop="$emit('preview')">
<i class="bi bi-airplane"></i>
</button>
</div>
</template>
</AppCard>
</template>
<script setup>
import { computed, inject } from 'vue'
const props = defineProps({
title: {
type: String,
required: true
},
content: {
type: String
},
createdAt: {
type: [String, Date, Number]
}
})
defineEmits(['modal', 'preview'])
const dayjs = inject('dayjs')
const createdDate = computed(() => dayjs(props.createdAt).format('YYYY. MM. DD HH:mm:ss'))
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,45 @@
<template>
<AppModal v-model="show" title="게시글">
<template #default>
<div class="row g-3">
<div class="col-3 text-muted">제목</div>
<div class="col-9">{{ title }}</div>
<div class="col-3 text-muted">내용</div>
<div class="col-9">{{ content }}</div>
<div class="col-3 text-muted">등록일</div>
<div class="col-9">
{{ $dayjs(createdAt).format('YYYY. MM. DD HH:mm:ss') }}
</div>
</div>
</template>
<template #actions>
<button type="button" class="btn btn-secondary" @click="closeModal">
닫기
</button>
</template>
</AppModal>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
modelValue: Boolean,
title: String,
content: String,
createdAt: [String, Number]
})
const emit = defineEmits(['update:modelValue'])
const show = computed({
get() {
return props.modelValue
},
set(value) {
emit('update:modelValue', value)
}
})
const closeModal = () => (show.value = false)
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,13 @@
import { useAlertStore } from '@/stores/alert'
import { storeToRefs } from 'pinia'
export function useAlert() {
const { alerts } = storeToRefs(useAlertStore())
const { vAlert, vSuccess } = useAlertStore()
return {
alerts,
vAlert,
vSuccess
}
}

View File

@ -0,0 +1,11 @@
import { computed, unref } from 'vue'
export const useNumber = (number) => {
const isOdd = computed(() => unref(number) % 2 === 1)
const isEven = computed(() => !isOdd.value)
return {
isOdd,
isEven
}
}

View File

@ -0,0 +1,5 @@
function color(el, binding) {
el.style.color = binding.value
}
export default color

View File

@ -0,0 +1,5 @@
export default {
mounted: el => {
el.focus()
}
}

View File

View File

View File

@ -0,0 +1,66 @@
import axios from 'axios'
import { isRef, ref, unref, watchEffect } from 'vue'
axios.defaults.baseURL = import.meta.env.VITE_APP_API_URL
const defaultConfig = {
method: 'get'
}
const defaultOptions = {
immediate: true
}
export const useAxios = (url, config = {}, options = {}) => {
const response = ref(null)
const data = ref(null)
const error = ref(null)
const loading = ref(false)
const { onSuccess, onError, immediate } = { ...defaultOptions, ...options }
const { params } = config
const execute = (body) => {
data.value = null
error.value = null
loading.value = true
axios(unref(url), {
...defaultConfig,
...config,
params: unref(params),
data: typeof body === 'object' ? body : {}
})
.then((res) => {
response.value = res
data.value = res.data
if (onSuccess) {
onSuccess(res)
}
})
.catch((err) => {
error.value = err
if (onError) {
onError(err)
}
})
.finally(() => {
loading.value = false
})
}
if (isRef(params) || isRef(url)) {
watchEffect(execute)
} else {
if (immediate) {
execute()
}
}
return {
response,
data,
error,
loading,
execute
}
}

View File

@ -0,0 +1,55 @@
<template>
<header>
<nav class="navbar navbar-expand-sm navbar-dark bg-primary">
<div class="container-fluid">
<RouterLink class="navbar-brand" to="/">ELD</RouterLink>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<RouterLink class="nav-link" active-class="active" to="/">Home</RouterLink>
</li>
<li class="nav-item">
<RouterLink class="nav-link" active-class="active" to="/about">about</RouterLink>
</li>
<li class="nav-item">
<RouterLink class="nav-link" active-class="active" to="/posts">게시글</RouterLink>
</li>
<li class="nav-item">
<RouterLink class="nav-link" active-class="active" to="/Nested">Nested</RouterLink>
</li>
<li class="nav-item">
<RouterLink class="nav-link" active-class="active" to="/my">MyPage</RouterLink>
</li>
</ul>
<div class="d-flex" role="search">
<button class="btn btn-outline-light" type="button" @click="goPage">글쓰기</button>
</div>
</div>
</div>
</nav>
</header>
</template>
<script setup>
import { useRouter } from 'vue-router'
const router = useRouter()
const goPage = () => {
router.push({
name: 'PostCreate'
})
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,11 @@
<template>
<main>
<div class="container py-4">
<RouterView></RouterView>
</div>
</main>
</template>
<script setup></script>
<style lang="scss" scoped></style>

45
allscore/src/main.js Normal file
View File

@ -0,0 +1,45 @@
// import './assets/main.css'
// import 'bootstrap/dist/css/bootstrap.min.css'
// import 'bootstrap-icons/font/bootstrap-icons.css'
// import { createApp } from 'vue'
// import App from './App.vue'
// import { createPinia } from 'pinia'
// import router from '@/router'
// import globalComponents from './plugins/global-components'
// import globalDirectives from './plugins/global-directives'
// import dayjs from './plugins/dayjs'
// const app = createApp(App)
// // app.directive('focus', focus)
// app.use(dayjs)
// app.use(globalDirectives)
// app.use(router)
// app.use(globalComponents)
// app.use(createPinia())
// app.mount('#app')
// import 'bootstrap/dist/js/bootstrap.js'
import './assets/main.css'
import 'bootstrap/dist/css/bootstrap.min.css'
import 'bootstrap-icons/font/bootstrap-icons.css'
import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'
import router from '@/router' // 라우터 임포트 다시 활성화
import globalComponents from './plugins/global-components'
import globalDirectives from './plugins/global-directives'
import dayjs from './plugins/dayjs'
const app = createApp(App)
app.use(dayjs)
app.use(globalDirectives)
app.use(router) // 라우터 사용
app.use(globalComponents)
app.use(createPinia())
app.mount('#app')
import 'bootstrap/dist/js/bootstrap.js'

View File

@ -0,0 +1,8 @@
import dayjs from 'dayjs'
export default {
install(app) {
app.config.globalProperties.$dayjs = dayjs
app.provide('dayjs', dayjs)
}
}

View File

@ -0,0 +1,5 @@
function funcPlugins(app, options) {
console.log('funcPlugins')
}
export default funcPlugins

View File

@ -0,0 +1,15 @@
import AppAlert from '@/components/app/AppAlert.vue'
import AppCard from '@/components/app/AppCard.vue'
import AppModal from '@/components/app/AppModal.vue'
import AppGrid from '@/components/app/AppGrid.vue'
import AppPagination from '@/components/app/AppPagination.vue'
export default {
install(app) {
app.component('AppAlert', AppAlert)
app.component('AppCard', AppCard)
app.component('AppModal', AppModal)
app.component('AppGrid', AppGrid)
app.component('AppPagination', AppPagination)
}
}

View File

@ -0,0 +1,9 @@
import focus from '@/directives/focus'
import color from '@/directives/color.js'
export default {
install(app) {
app.directive('focus', focus)
app.directive('color', color)
}
}

View File

@ -0,0 +1,12 @@
const objPlugins = {
install(app, options) {
console.log('objPlugins app: ', app)
console.log('objPlugins options: ', options)
// app.component() 전역 컴포넌트
// app.config.globalProperies 전역 애플리케이션 인스턴스에 속성 추가
// app.directive 커스텀 디렉티브
// app.provide 리소스
}
}
export default objPlugins

View File

@ -0,0 +1,13 @@
export default {
install(app, options) {
const person = {
name: '짐코딩',
say() {
alert(this.name)
},
...options
}
app.config.globalProperties.$person = person
app.provide('person', person)
}
}

View File

@ -0,0 +1,130 @@
// import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
// import HomeView from '@/views/HomeView.vue'
// import AboutView from '@/views/AboutView.vue'
// import PostCreateView from '@/views/posts/PostCreateView.vue'
// import PostDetailView from '@/views/posts/PostDetailView.vue'
// import PostEditView from '@/views/posts/PostEditView.vue'
// import PostListView from '@/views/posts/PostListView.vue'
// import NotFoundView from '@/views/NotFoundView.vue'
// import NestedView from '@/views/nested/NestedView.vue'
// import NestedOneView from '@/views/nested/NestedOneView.vue'
// import NestedTwoView from '@/views/nested/NestedTwoView.vue'
// import NestedHomeView from '@/views/nested/NestedHomeView.vue'
// import MyPage from '@/views/MyPage.vue'
// const routes = [
// {
// path: '/',
// name: 'home',
// component: HomeView
// },
// {
// path: '/about',
// name: 'about',
// component: AboutView
// },
// {
// path: '/posts',
// name: 'PostList',
// component: PostListView
// },
// {
// path: '/posts/create',
// name: 'PostCreate',
// component: PostCreateView
// },
// {
// path: '/posts/:id',
// name: 'PostDetail',
// component: PostDetailView,
// props: true
// // props: (route) => ({ id: parseInt(route.params.id) })
// },
// {
// path: '/posts/:id/edit',
// name: 'PostEdit',
// component: PostEditView
// },
// { path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFoundView },
// {
// path: '/nested',
// name: 'Nested',
// component: NestedView,
// children: [
// {
// path: '',
// name: 'NestedHome',
// component: NestedHomeView
// },
// {
// path: 'one',
// name: 'NestedOne',
// component: NestedOneView
// },
// {
// path: 'two',
// name: 'NestedTwo',
// component: NestedTwoView
// }
// ]
// },
// {
// path: '/my',
// name: 'MyPage',
// component: MyPage,
// beforeEnter: [removeQueryString]
// // console.log('to: ', to)
// // console.log('from: ', from)
// // return false
// // return { name: 'home' }
// }
// ]
// function removeQueryString(to) {
// if (Object.keys(to.query).length > 0) {
// return { path: to.path, query: {} }
// }
// }
// const router = createRouter({
// history: createWebHistory(),
// // history: createWebHashHistory('/base'),
// routes
// })
// // router.beforeEach((to, from) => {
// // console.log('to: ', to)
// // console.log('from: ', from)
// // if (to.name === 'MyPage') {
// // // return false
// // // return { name: 'home' }
// // return '/posts'
// // }
// // })
// export default router
import { createRouter, createWebHistory } from 'vue-router'
import Card from '@/card/Card.vue'
import CardTest from '@/card/CardTest.vue'
const routes = [
{
path: '/',
name: 'Card',
component: Card
},
{
path: '/test',
name: 'CardTest',
component: CardTest
}
]
const router = createRouter({
history: createWebHistory('/'), // process.env.BASE_URL 대신 '/' 사용
routes
})
export default router

View File

@ -0,0 +1,18 @@
import { defineStore } from 'pinia'
export const useAlertStore = defineStore('alert', {
state: () => ({
alerts: []
}),
actions: {
vAlert(message, type = 'error') {
this.alerts.push({ message, type })
setTimeout(() => {
this.alerts.shift()
}, 2000)
},
vSuccess(message) {
this.vAlert(message, 'success')
}
}
})

View File

@ -0,0 +1,18 @@
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
counter: 1
}),
getters: {
doubleCount: (state) => state.counter * 2,
doubleCountPlusOne() {
return this.doubleCount + 1
}
},
actions: {
increment() {
this.counter++
}
}
})

View File

@ -0,0 +1,29 @@
<template>
<div>
<h2>About View</h2>
<p>{{ $route.path }}</p>
<p>{{ $route.name }}</p>
<button class="btn btn-primary" @click="$router.push('/')">Home으로 이동</button>
<h2>Store</h2>
<p>counter: {{ counter }}</p>
<p>doubleCount: {{ doubleCount }}</p>
<p>doubleCount: {{ doubleCountPlusOne }}</p>
<button @click="increment()">Click!</button>
</div>
</template>
<script setup>
import { useRoute } from 'vue-router'
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'
const route = useRoute()
console.log('route.pathe:, ', route.path)
const store = useCounterStore()
const { counter, doubleCount, doubleCountPlusOne } = storeToRefs(store)
const { increment } = store
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,50 @@
<template>
<div>
<h2>Home View</h2>
<p>{{ $route.path }}</p>
<p>{{ $route.name }}</p>
<button class="btn btn-primary" @click="goAboutPage">About으로 이동</button>
<hr class="my-4" />
<AppGrid :items="items" v-slot="{ item }" col-class="col-6">
<AppCard>{{ item }}</AppCard>
</AppGrid>
<hr class="my-4" />
<!-- <h2>{{ $person.name }}</h2>
<button class="btn btn-primary" @click="person.say">click person</button> -->
<h2>{{ position }}</h2>
<h2>x: {{ x }} y: {{ y }}</h2>
</div>
</template>
<script>
export default {
created() {
// console.log(this.$person.name)
// this.$person.say()
}
}
</script>
<script setup>
import { reactive, ref, toRef, toRefs } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const goAboutPage = () => {
router.push('/about')
}
const items = ref(['사과', '딸기', '포토', '바나나'])
// const person = inject('person')
// console.log('person.name: ', person.name)
const position = reactive({
x: 100,
y: 1000
})
// const x = toRef(position, 'x')
// const y = toRef(position, 'y')
const { x, y } = toRefs(position)
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,10 @@
<template>
<div>
<h2>MyPage</h2>
<hr />
</div>
</template>
<script setup></script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,17 @@
<template>
<div class="text-center py-5">
<h1>Oops!</h1>
<h2>404 Not Found</h2>
<div class="text-muted">요청한 페이지를 찾을 없습니다!</div>
<!-- {{ $route.params.pathMatch }} -->
<div class="mt-4">
<RouterLink to="/">
<button class="btn btn-primary">Home</button>
</RouterLink>
</div>
</div>
</template>
<script setup></script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,12 @@
<template>
<div class="card">
<div class="card-header">Nested Home</div>
<div class="card-body">
<h1 class="card-title">Nested routes home...</h1>
</div>
</div>
</template>
<script setup></script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,16 @@
<template>
<div class="card">
<div class="card-header">Nested One</div>
<div class="card-body">
<h5 class="card-title">Special title treatment</h5>
<p class="card-text">
With supporting text below as a natural lead-in to additional content.
</p>
<a href="#" class="btn btn-danger">Go somewhere</a>
</div>
</div>
</template>
<script setup></script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,16 @@
<template>
<div class="card">
<div class="card-header">Nested Two</div>
<div class="card-body">
<h5 class="card-title">Special title treatment</h5>
<p class="card-text">
With supporting text below as a natural lead-in to additional content.
</p>
<a href="#" class="btn btn-warning">Go somewhere</a>
</div>
</div>
</template>
<script setup></script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,30 @@
<template>
<div>
<ul class="nav nav-pills">
<li class="nav-item">
<RouterLink
class="nav-link"
active-class="active"
:to="{ name: 'NestedOne', replace: true }"
>
Nested One
</RouterLink>
</li>
<li class="nav-item">
<RouterLink
class="nav-link"
active-class="active"
:to="{ name: 'NestedTwo', replace: true }"
>
Nested Two
</RouterLink>
</li>
</ul>
<hr />
<RouterView></RouterView>
</div>
</template>
<script setup></script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,88 @@
<template>
<div>
<h2 @click="visibleForm = !visibleForm">게시글 등록</h2>
<hr class="my-4" />
<AppError v-if="error" :message="error.message" />
<PostForm
v-if="visibleForm"
v-model:title="form.title"
v-model:content="form.content"
@submit.prevent="save"
>
<template #actions>
<button type="button" class="btn btn-outline-dark me-2" @click="goListPage">목록</button>
<button class="btn btn-primary" :disabled="loading">
<template v-if="loading">
<span class="spinner-grow spinner-grow-sm" aria-hidden="true"></span>
<span class="visually-hidden" role="status">Loading...</span>
</template>
<template v-else> 저장 </template>
</button>
</template>
</PostForm>
</div>
</template>
<script setup>
import { createPost } from '@/api/posts'
import { ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import PostForm from '@/components/posts/PostForm.vue'
import { useAlert } from '@/composables/alert'
import { useAxios } from '@/hooks/useAxios'
const { vAlert, vSuccess } = useAlert()
const route = useRoute()
const router = useRouter()
const form = ref({
title: null,
content: null
})
const { error, loading, execute } = useAxios(
'/posts',
{
method: 'post'
},
{
immediate: false,
onSuccess: () => {
router.push({ name: 'PostList' })
vSuccess('등록이 완료되었습니다.')
},
onError: (err) => {
vAlert(err.message)
}
}
)
const save = async () => {
execute({ ...form.value, createdAt: Date.now() })
}
// const save = async () => {
// try {
// loading.value = true
// await createPost({
// ...form.value,
// createdAt: Date.now()
// })
// router.push({ name: 'PostList' })
// vSuccess(' .')
// } catch (err) {
// vAlert(err.message)
// error.value = err
// } finally {
// loading.value = false
// }
// }
// const id = route.params.id
const goListPage = () => router.push({ name: 'PostList' })
const visibleForm = ref(true)
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,114 @@
<template>
<AppLoading v-if="loading" />
<AppError v-else-if="error" :message="error.message" />
<div v-else>
<h2>{{ post.title }}</h2>
<p>id: {{ props.id }}, isOdd: {{ isOdd }}</p>
<p>{{ post.content }}</p>
<p class="text-muted">
{{ $dayjs(post.createdAt).format('YYYY. MM. DD HH:mm:ss') }}
</p>
<hr class="my-4" />
<AppError v-if="removeError" :message="removeError.message" />
<div class="row">
<div class="col-auto">
<button class="btn btn-outline-dark" @click="$router.push('/posts/18')">이전글</button>
</div>
<div class="col-auto">
<button class="btn btn-outline-dark" @click="$router.push('/posts/20')">다음글</button>
</div>
<div class="col-auto me-auto"></div>
<div class="col-auto">
<button class="btn btn-outline-dark" @click="gotListPage">목록</button>
</div>
<div class="col-auto">
<button class="btn btn-outline-primary" @click="goEditPage">수정</button>
</div>
<div class="col-auto">
<button class="btn btn-outline-danger" @click="remove" :disabled="removeLoading">
<template v-if="removeLoading">
<span class="spinner-grow spinner-grow-sm" aria-hidden="true"></span>
<span class="visually-hidden" role="status">Loading...</span>
</template>
<template v-else> 삭제 </template>
</button>
</div>
</div>
</div>
</template>
<script setup>
import { onBeforeRouteLeave, onBeforeRouteUpdate, useRouter } from 'vue-router'
import { deletePost } from '@/api/posts'
import { computed, ref, toRef, toRefs } from 'vue'
import { useAlert } from '@/composables/alert'
import { useAxios } from '@/hooks/useAxios'
import { useNumber } from '@/composables/number'
const { vAlert, vSuccess } = useAlert()
const props = defineProps({
id: [String, Number]
})
// const route = useRoute()
const router = useRouter()
const { id: idref } = toRefs(props)
const { isOdd } = useNumber(idref)
// const id = route.params.id
const url = computed(() => `/posts/${props.id}`)
const { data: post, error, loading } = useAxios(url)
const {
error: removeError,
loading: removeLoading,
execute
} = useAxios(
`/posts/${props.id}`,
{ method: 'delete' },
{
immediate: false,
onSuccess: () => {
vSuccess('삭제가 완료되었습니다.')
router.push({ name: 'PostList' })
},
onError: (err) => {
vAlert(err.message)
}
}
)
const remove = async () => {
if (confirm('삭제 하시겠습니까?') === false) {
return
}
execute()
}
const gotListPage = () => {
router.push({ name: 'PostList' })
}
const goEditPage = () => {
router.push({
name: 'PostEdit',
params: { id: props.id }
})
}
onBeforeRouteUpdate(() => {
console.log('onBeforeRouteUpdate')
})
onBeforeRouteLeave(() => {
console.log('onBeforeRouteLeave')
})
</script>
<script>
export default {
beforeRouteEnter() {
console.log('beforeRouteEnter')
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,69 @@
<template>
<AppLoading v-if="loading" />
<AppError v-else-if="error" :message="error.message" />
<div v-else>
<h2>게시글 수정</h2>
<hr class="my-4" />
<AppError v-if="editError" :message="editError.message" />
<PostForm v-model:title="form.title" v-model:content="form.content" @submit.prevent="edit">
<template #actions>
<button type="button" class="btn btn-outline-danger" @click="goDetailPage">취소</button>
<button class="btn btn-primary" :disabled="editLoading">
<template v-if="editLoading">
<span class="spinner-grow spinner-grow-sm" aria-hidden="true"></span>
<span class="visually-hidden" role="status">Loading...</span>
</template>
<template v-else> 수정 </template>
</button>
</template>
</PostForm>
<!-- <AppAlert :show="showAlert" :message="alertMessage" :type="alertType" /> -->
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { getPostById, updatePost } from '@/api/posts'
import PostForm from '@/components/posts/PostForm.vue'
import { useAlert } from '@/composables/alert'
import { useAxios } from '@/hooks/useAxios'
const { vAlert, vSuccess } = useAlert()
const route = useRoute()
const router = useRouter()
const id = route.params.id
const { data: form, error, loading } = useAxios(`/posts/${id}`)
const {
error: editError,
loading: editLoading,
execute
} = useAxios(
`/posts/${id}`,
{ method: 'patch' },
{
immediate: false,
onSuccess: () => {
vSuccess('수정이 완료되었습니다!', 'success')
router.push({ name: 'PostDetail', params: { id } })
},
onError: (err) => {
vAlert(err.message)
}
}
)
const edit = () => {
execute({
...form.value
})
}
const goDetailPage = () => router.push({ name: 'PostDetail', params: { id } })
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,129 @@
<template>
<div>
<h2>게시글 목록</h2>
<hr class="my-4" />
<PostFilter
v-model:title="params.title_like"
v-model:limit="params._limit"
@update:limit="changeLimit"
/>
<hr class="my-4" />
<AppLoading v-if="loading" />
<AppError v-else-if="error" :message="error.message" />
<template v-else-if="!isExist">
<p class="text-center py-5 text-muted">No Results</p>
</template>
<template v-else>
<AppGrid :items="posts" col-class="col-12 col-sd-6 col-lg-4">
<template v-slot="{ item }">
<PostItem
:title="item.title"
:content="item.content"
:createdAt="item.createdAt"
@click="goPage(item.id)"
@modal="openModal(item)"
@preview="selectPreview(item.id)"
></PostItem>
</template>
</AppGrid>
<AppPagination
:current-page="params._page"
:page-count="pageCount"
@page="(page) => (params._page = page)"
/>
</template>
<Teleport to="#modal">
<PostModal
v-model="show"
:title="modalTitle"
:content="modalContent"
:created-at="modalCreatedAt"
/>
</Teleport>
<template v-if="previewId">
<hr class="my-5" />
<AppCard>
<PostDetailView :id="previewId"></PostDetailView>
</AppCard>
</template>
</div>
</template>
<script setup>
import PostDetailView from '@/views/posts/PostDetailView.vue'
import PostItem from '@/components/posts/PostItem.vue'
import PostFilter from '@/components/posts/PostFilter.vue'
import PostModal from '@/components/posts/PostModal.vue'
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useAxios } from '@/hooks/useAxios'
const router = useRouter()
const previewId = ref(null)
const selectPreview = (id) => (previewId.value = id)
const params = ref({
_sort: 'createdAt',
_order: 'desc',
_page: 1,
_limit: 6,
title_like: ''
})
const changeLimit = (value) => {
params.value._limit = value
params.value._page = 1
}
const { response, data: posts, error, loading } = useAxios('/posts', { method: 'get', params })
const isExist = computed(() => posts.value && posts.value.length)
//
const totalCount = computed(() => response.value.headers['x-total-count'])
const pageCount = computed(() => Math.ceil(totalCount.value / params.value._limit))
// const fetchPosts = async () => {
// try {
// loading.value = true
// const { data, headers } = await getPosts(params.value)
// posts.value = data
// totalCount.value = headers['x-total-count']
// } catch (err) {
// error.value = err
// } finally {
// loading.value = false
// }
// }
// // fetchPosts()
// watchEffect(fetchPosts)
const goPage = (id) => {
// router.push(`/posts/${id}`)
router.push({
name: 'PostDetail',
params: { id: id },
query: { searchText: 'hello' },
hash: '#world!'
})
}
//modal
const show = ref(false)
const modalTitle = ref('')
const modalContent = ref('')
const modalCreatedAt = ref('')
const openModal = ({ title, content, createdAt }) => {
show.value = true
modalTitle.value = title
modalContent.value = content
modalCreatedAt.value = createdAt
}
const closeModal = () => (show.value = false)
</script>
<style lang="scss" scoped></style>

25
allscore/vite.config.js Normal file
View File

@ -0,0 +1,25 @@
import { fileURLToPath, URL } from 'node:url'
import Components from 'unplugin-vue-components/vite'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
Components({
dirs: [''],
dts: true
})
],
// mode: 'production',
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
build: {
outDir: 'dist'
}
})

14
docker-compose.yml Normal file
View File

@ -0,0 +1,14 @@
version: '3'
services:
nginx:
image: nginx:latest
container_name: vue-nginx
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
- /home/ec2-user/eld/card_service/frontend/DIST_240708:/usr/share/nginx/html
- /home/ec2-user/eld/card_service/frontend/eld.crt:/home/ec2-user/eld/card_service/frontend/eld.crt
- /home/ec2-user/eld/card_service/frontend/eld.key:/home/ec2-user/eld/card_service/frontend/eld.key
- /home/ec2-user/eld/card_service/frontend/56DA6C13A51666386B7D2E61E85EF2E2.txt:/home/ec2-user/eld/card_service/frontend/56DA6C13A51666386B7D2E61E85EF2E2.txt
ports:
- "80:80"
- "443:443"

35
eld.crt Normal file
View File

@ -0,0 +1,35 @@
-----BEGIN CERTIFICATE-----
MIIGCDCCBPCgAwIBAgIQWwEQbfqkdYEA35goU9JMADANBgkqhkiG9w0BAQsFADBM
MQswCQYDVQQGEwJMVjENMAsGA1UEBxMEUmlnYTERMA8GA1UEChMIR29HZXRTU0wx
GzAZBgNVBAMTEkdvR2V0U1NMIFJTQSBEViBDQTAeFw0yNDA3MjMwMDAwMDBaFw0y
NTA3MjMyMzU5NTlaMBYxFDASBgNVBAMTC2VsZHNvZnQuY29tMIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkYA7IYc330ya/BFBhcG8QQrdKuV5GPV5rh0K
NIeCU0yWl5ydYzpXhWuFE/qnQfVRK5g+1jAfBK4UHTZdpdvWzdDlZFkJBiQO0MS2
/ujWpTWSEYqalgRxZylTDSOG+KAt+Tpo0ZWv/T6okjW54+J2vi4E/2QGPzi/Dr5c
d9hlMBB3VREsmDqDScVnHOlxbMVLf/EgIEP5CbGwdV71/R5Tpcdkr9ubw5s1RdMz
GEYfQEJM56zdlAwhG7ucadoIVXRlP6BWmyrk9uZgcDg1waUoFekyCGg/gwhdzoH5
xglDwb1tL/b+jxhgm8Cu0Av5GwaD45LSc1B1+AHM99qnoETuKQIDAQABo4IDGjCC
AxYwHwYDVR0jBBgwFoAU+ftQxItnu2dk/oMhpqnOP1WEk5kwHQYDVR0OBBYEFLxH
VZ+saqBmJr8jEwI3UhYLh4DLMA4GA1UdDwEB/wQEAwIFoDAMBgNVHRMBAf8EAjAA
MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjBLBgNVHSAERDBCMDYGCysG
AQQBsjEBAgJAMCcwJQYIKwYBBQUHAgEWGWh0dHBzOi8vY3BzLnVzZXJ0cnVzdC5j
b20wCAYGZ4EMAQIBMD0GA1UdHwQ2MDQwMqAwoC6GLGh0dHA6Ly9jcmwudXNlcnRy
dXN0LmNvbS9Hb0dldFNTTFJTQURWQ0EuY3JsMG8GCCsGAQUFBwEBBGMwYTA4Bggr
BgEFBQcwAoYsaHR0cDovL2NydC51c2VydHJ1c3QuY29tL0dvR2V0U1NMUlNBRFZD
QS5jcnQwJQYIKwYBBQUHMAGGGWh0dHA6Ly9vY3NwLnVzZXJ0cnVzdC5jb20wFgYD
VR0RBA8wDYILZWxkc29mdC5jb20wggGABgorBgEEAdZ5AgQCBIIBcASCAWwBagB2
AN3cyjSV1+EWBeeVMvrHn/g9HFDf2wA6FBJ2Ciysu8gqAAABkN5FaiIAAAQDAEcw
RQIgHTbwtmQN6xLMETdF+8eqQ7AKqXO8N2f+bD7Fx7tzlo4CIQDc3kmta7RofW+L
jZxQtI4p3b4hFUFCDw85ibQgCSpH6QB3AA3h8jAr0w3BQGISCepVLvxHdHyx1+kw
7w5CHrR+Tqo0AAABkN5Fae8AAAQDAEgwRgIhAI2ZgaTuuAjcfrg6o2KeX+O8ga8e
tGY7+NZRt/yRXuyXAiEA5bEkR2LchqMV3vYr5Eo/7nKm8eTXNBhduxNIPxNzRoAA
dwAS8U40vVNyTIQGGcOPP3oT+Oe1YoeInG0wBYTr5YYmOgAAAZDeRWnNAAAEAwBI
MEYCIQDT4+jvqlZboMs6AeqxZRu0TyKB/zA5UzGfJdaCaWQ8iAIhAKgg7Q+W0sd/
Gxaa2pHXpHSxP3sfkT9o5Br8S4pILca3MA0GCSqGSIb3DQEBCwUAA4IBAQA51hYf
mw+kueMcWmPUlUikrK0/KuLpSfWPvHOj+r84Y5AS9JxZmi9b+9W18p1VwO0YXR6U
ho4PyHjuIviT2LviAWyvRTFc9Il+e920+RyMiAdfQ/Af15xdcAJOjSMlZu3xkqvW
gkgE1kPvIQevqCmEEzUqsUVEn7ftoDcT9SEJBpLFwB6FAFBfw0pvJ0qUr9LAin4p
bklWGaY2laI2py7MXZNUO35rahA0DNS/Y2sY3TbH/WXilJ/sbJ9sO83/XC2T7vgC
Le2cv9gNFJPiTW++OROxybXzarwIwWFHO7aPc5Ehv9U54/XlzBIzrRsJuvtXkc0I
0nuiAfpjOom89Z1W
-----END CERTIFICATE-----

17
eld.csr Normal file
View File

@ -0,0 +1,17 @@
-----BEGIN CERTIFICATE REQUEST-----
MIICrzCCAZcCAQAwbDEUMBIGA1UEAwwLZWxkc29mdC5jb20xHzAdBgNVBAoMFuyj
vOyLne2ajOyCrCDsnbTsl5jrlJQxEjAQBgNVBAgMCeuMgOyghOyLnDESMBAGA1UE
BwwJ7Jyg7ISx6rWsMQswCQYDVQQGEwJLUjCCASIwDQYJKoZIhvcNAQEBBQADggEP
ADCCAQoCggEBAJGAOyGHN99MmvwRQYXBvEEK3SrleRj1ea4dCjSHglNMlpecnWM6
V4VrhRP6p0H1USuYPtYwHwSuFB02XaXb1s3Q5WRZCQYkDtDEtv7o1qU1khGKmpYE
cWcpUw0jhvigLfk6aNGVr/0+qJI1uePidr4uBP9kBj84vw6+XHfYZTAQd1URLJg6
g0nFZxzpcWzFS3/xICBD+QmxsHVe9f0eU6XHZK/bm8ObNUXTMxhGH0BCTOes3ZQM
IRu7nGnaCFV0ZT+gVpsq5PbmYHA4NcGlKBXpMghoP4MIXc6B+cYJQ8G9bS/2/o8Y
YJvArtAL+RsGg+OS0nNQdfgBzPfap6BE7ikCAwEAATANBgkqhkiG9w0BAQsFAAOC
AQEAZhOyuc//t8Vp+Iog/C12fFSIB4baHBS/GV3C/xm1gvmn/Vl8ynaswPA3ei0U
rvoXidZHYt7gdcBq1DlPDGxbnEwNYZhNFR0Sv622EO9MrsiUsje6yWm2xmCgM8zw
ChhifmEulSBEFcOQGi9eKA19jJHKEqRx5QhlZHsRdVD+glxyESpOM0cfUXGsYXhn
Pvm9EvlZfYyoVBRjIwO3CIKSaGsgg2FKUKnmgMo28BKVPYM1/gNC47ozhwMOgDrO
fg1KZ3CtOMn3ZG7OiN2HamnBNKQDDp6mZJT1aoq1GsENYFNJUfW768G2rmAUGNfw
My3PBjidoiuChvimG1Fqpnioxw==
-----END CERTIFICATE REQUEST-----

27
eld.key Normal file
View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAkYA7IYc330ya/BFBhcG8QQrdKuV5GPV5rh0KNIeCU0yWl5yd
YzpXhWuFE/qnQfVRK5g+1jAfBK4UHTZdpdvWzdDlZFkJBiQO0MS2/ujWpTWSEYqa
lgRxZylTDSOG+KAt+Tpo0ZWv/T6okjW54+J2vi4E/2QGPzi/Dr5cd9hlMBB3VREs
mDqDScVnHOlxbMVLf/EgIEP5CbGwdV71/R5Tpcdkr9ubw5s1RdMzGEYfQEJM56zd
lAwhG7ucadoIVXRlP6BWmyrk9uZgcDg1waUoFekyCGg/gwhdzoH5xglDwb1tL/b+
jxhgm8Cu0Av5GwaD45LSc1B1+AHM99qnoETuKQIDAQABAoIBAAhxYU0CYoe4s2AX
1L5hFe5MxfVqpCaifOVxbjGK4PE6Nx1UU04qGSDG8s2MXIb/aA7AanoFiBE+lDB3
QoMgsPPXsK3sXDGQ51KuLYO4aVckFwYhTbPRjW6L53OyWW9FJTHKdcFenxwR9hho
2XDrp9IEi9nxeQrTXUPK4ET8h6+cmHgEDEXJ18xyn3EMrXbBTOBaAGJwdA/kE947
o85Yu+o3sKA+3JsuuMijSy2XSqvslNNeyOvlcFcq88eDY5pXuDGbRQPnIQe3t0Fc
DMtIF2E2jOtIbMuBdcdZaUduiPqVRT3ojW2LaDc58H7+piAgBUVGJCLuqIgujBa0
t3n9M3ECgYEA1oe+oVG8+OVvcg9IJyQmnZOIR3hopuDvZOCYuSOsOS8DMo+p066z
6rSKpvtIcJJ8l0mM8Rw4WifEbEYOgkt9yGVK80mMI2BkSwmq5U4hOWg/xxqATTz5
PBgz0bBwZ6ZgW/3RD+jWybX3vy+beifAL3v52q+hnZdZf1q1rBYYWXECgYEAraB/
C/a4RO0LygrncUgn9MX4mGe2zRVAszZcbcX2iG4o1mseoZ16HO1ipS4Q/kI/znpY
r7XT4QgF98Fdkcl8MYKP+snUv9TG7zOFiYOe0JeaafBuukMDPUqeQXk/d9rSrJg9
lqiw1LYYyxPnh8aTEfwMBPqaRhLiMeD63PsHRDkCgYEAzwflUi1dnx1b9ckFqrBa
i8tqwv5SkGmW3dVZzaG9fNn/zfWSwPRiMOjWvdrWx7y2fBHA8JZ5U5f5GTxqmBde
ZdxK/opFsYY+g6PqxqwlqA8RLYZHt0JWjEYXDA+oCn8nkt9ZuG7NiZAQbPL2qmZe
M/UC5KaF413CQwM5O79+9CECgYBT/D+YNOabiKJcP/wGAuY48441gm2dNDuQtKnu
+4QuKEMevMAbYwZPedBuoCLeKoOcx/egPu7Xej8QwfsV6wVlGYe1wu1jQXRc/moI
w58NvVeXCRM2i/XELxTwDMtTmYiwrg+UkdK/gbnqeZ1UQwye9XGG8wWvAbFieTY/
sDmqmQKBgQCUyu98Gpatoe8MdyHZf9h+QfYcUkpLuhJOvWCGQt9QyfiIal5eleXG
YPNaQBk3GtSPyuB1nDrjtHaIlyw1ScjRCfzth3VJvwWnqz9y7XP2pow2+TnY0Jco
lvMRY9n55TgAPdTdgHshUyDRAM1/aUbiBIPPOP/XAcmONZ1lpayZ7A==
-----END RSA PRIVATE KEY-----

40
nginx.conf Normal file
View File

@ -0,0 +1,40 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
# DCV 검증을 위한 추가 설정
location = /.well-known/pki-validation/56DA6C13A51666386B7D2E61E85EF2E2.txt {
alias /home/ec2-user/eld/card_service/frontend/56DA6C13A51666386B7D2E61E85EF2E2.txt;
}
error_page 404 /index.html;
}
server {
listen 443 ssl;
server_name _;
ssl_certificate /home/ec2-user/eld/card_service/frontend/eld.crt;
ssl_certificate_key /home/ec2-user/eld/card_service/frontend/eld.key;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
# DCV 검증을 위한 추가 설정
location = /.well-known/pki-validation/56DA6C13A51666386B7D2E61E85EF2E2.txt {
alias /home/ec2-user/eld/card_service/frontend/56DA6C13A51666386B7D2E61E85EF2E2.txt;
}
error_page 404 /index.html;
}