app
├ /routes
├ /views
├ /files
├ /public
├ /tmps
├ /modules
├ .env
└ index.js
.env
SESSION_SECRET=세션키(ex: session-key)
SERVER_PORT=http포트(ex: 3000)
DB_HOST=데이터베이스호스트네임(ex: localhost)
DB_PORT=데이터베이스포트(ex: 3306)
DB_USER=데이터베이스사용자
DB_PASSWORD=데이터베이스비밀번호
DB_DATABASE=데이터베이스이름
HASH_FUNCTION=암호방식(ex: SHA2)
HASH_LENGTH=암호길이(ex: 256)
index.js
import express from 'express';
import ejs from 'ejs';
import dotenv from 'dotenv';
import http from 'http';
import path from 'path';
import middleware from './modules/middleware.js';
import routes from './routes/index.js';
dotenv.config();
const app = express();
app.set('view engine', 'ejs');
app.set('views', `${path.resolve()}/views`);
middleware(app);
app.use(routes);
http.createServer(app).listen(process.env.SERVER_PORT, () => {
console.log('웹서버가 실행 중입니다.');
});
middleware.js
import express from 'express';
import session from 'express-session';
import dotenv from 'dotenv';
import path from 'path';
dotenv.config();
const sessionOptions = {
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: true,
cookie: {
secure: false,
maxAge: 1000 * 60 * 60 * 24
}
};
async function validateSession(req, res, next) {
const user = req.session.user;
const pathName = req.path;
if (pathName === '/login' || pathName === '/logout' || pathName === '/favicon.ico') {
return next();
}
if (user) {
//로그인 성공 처리
} else {
return res.redirect('/login');
}
}
function middleware (app) {
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use('/public', express.static(`${path.resolve()}/public`));
app.use('/tmps', express.static(`${path.resolve()}/tmps`));
app.use(session(sessionOptions));
app.use(validateSession);
app.use('/files', express.static(`${path.resolve()}/files`));
}
export default middleware;
db.js
import knex from 'knex';
import dotenv from 'dotenv';
dotenv.config();
const db = knex({
client: 'mysql2',
connection: {
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
},
});
export default db;
index.js
import express from 'express';
const router = express.Router();
import login from './login.js';
router.use('/login', login);
import logout from './logout.js';
router.use('/logout', logout);
import home from './home.js';
router.use('/', home);
export default router;
example.js
import express from 'express';
const router = express.Router();
router.get('/', (req, res) => {
return res.render('example', { data });
});
router.post('/', (req, res) => {
return res.status(204).end();
});
export default router;
layout1.ejs
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link rel="manifest" href="/public/manifest.json">
<link rel="apple-touch-icon" size="512x512" href="/public/images/ios.png">
<link rel="stylesheet" href="/public/styles/index.css">
<% if (typeof styles !== 'undefined') { %>
<% for (const style of styles) { %>
<link rel="stylesheet" href="/public/styles/<%= style %>.css">
<% } %>
<% } %>
<title>타이틀</title>
</head>
<body>
<header>
<svg id="menu_toggle" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M120-240v-80h720v80H120Zm0-200v-80h720v80H120Zm0-200v-80h720v80H120Z"/></svg>
</header>
<nav></nav>
<article>
layout2.ejs
</article>
<section><progress id="progress" min="0" max="100"></progress></section>
<footer></footer>
<script src="/public/scripts/index.js"></script>
<% if (typeof scripts !== 'undefined') { %>
<% for (const script of scripts) { %>
<script type="<%= typeof script.type === 'undefined' ? 'text/javascript' : script.type %>" src="/public/scripts/<%= script.src %>.js"></script>
<% } %>
<% } %>
</body>
</html>
example.ejs
<%- include('layout1', { styles: ['example'] }) -%>
<%- include('layout2', { scripts: [{ src: 'example', type: 'module' }] }) -%>
manifest.json
{
"name": "앱이름",
"start_url": "/",
"display": "fullscreen",
"icons": [
{
"src": "images/android.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
index.css
:root {
--color-primary: #7bbfa7;
--color-primary-dark: #2f5d4e;
--color-primary-light: #d0f0e3;
--color-gray-900: #111827;
--color-gray-700: #374151;
--color-gray-500: #6b7280;
--color-gray-300: #d1d5db;
--color-gray-100: #f3f4f6;
--color-text: #111827;
--color-text-muted: #6b7280;
--header-height: 50px;
--footer-height: 30px;
--padding: 0.5em;
}
* {
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
header {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: var(--header-height);
}
nav {
position: fixed;
left: 0;
top: var(--header-height);
height: calc(100% - var(--header-height) - var(--footer-height));
display: flex;
flex-direction: column;
}
nav a {
padding: var(--padding);
color: var(--color-text-muted);
text-align: center;
}
nav a:hover {
background: var(--color-gray-100);
color: var(--color-text);
}
section,
article {
position: fixed;
top: var(--header-height);
left: 0;
width: 100%;
height: calc(100% - var(--header-height) - var(--footer-height));
}
article {
z-index: 2;
overflow: auto;
}
section {
display: flex;
justify-content: center;
align-items: center;
z-index: 1;
}
footer {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
height: var(--footer-height);
}
a {
text-decoration: none;
}
.sticky {
position: sticky;
}
@media only screen and (orientation: landscape) {
}
@media only screen and (orientation: portrait) {
}
app.locals.todayOrTomorrow = (inputDateStr) => {
const seoul = DateTime.now().setZone('Asia/Seoul');
const today = seoul.toISODate();
const tomorrow = seoul.plus({ days: 1 }).toISODate();
if (inputDateStr === today) return '오늘';
if (inputDateStr === tomorrow) return '내일';
return '해당 없음';
};