이 블로그에서는 프런트엔드에 Angular를 사용하고 스타일링에 Tailwind CSS를 사용하여 URL 단축기 애플리케이션을 만드는 과정을 안내합니다. URL 단축기는 긴 URL을 더 짧고 관리하기 쉬운 링크로 변환하는 편리한 도구입니다. 이 프로젝트는 최신 웹 개발 기술을 사용하여 기능적이고 미학적으로 만족스러운 웹 애플리케이션을 구축하는 방법을 이해하는 데 도움이 될 것입니다.
이 튜토리얼을 진행하려면 Angular에 대한 기본적인 이해와 Tailwind CSS에 어느 정도 익숙해야 합니다. 컴퓨터에 Node.js와 Angular CLI가 설치되어 있는지 확인하세요.
먼저 터미널에서 다음 명령을 실행하여 새 Angular 프로젝트를 만듭니다.
ng new url-shortener-app cd url-shortener-app
다음으로 Angular 프로젝트에서 Tailwind CSS를 설정합니다. npm을 통해 Tailwind CSS 및 해당 종속성을 설치합니다.
npm install -D tailwindcss postcss autoprefixer npx tailwindcss init
tailwind.config.js 파일을 업데이트하여 Tailwind CSS를 구성하세요.
module.exports = { content: [ "./src/**/*.{html,ts}", ], theme: { extend: {}, }, plugins: [], }
Src/styles.scss 파일에 Tailwind 지시어를 추가하세요.
@tailwind base; @tailwind components; @tailwind utilities;
URL 모델을 생성하여 URL 데이터의 구조를 정의합니다. 새 파일 src/app/models/url.model.ts 추가:
export type Urls = Url[]; export interface Url { _id: string; originalUrl: string; shortUrl: string; clicks: number; expirationDate: string; createdAt: string; __v: number; }
URL 단축과 관련된 API 호출을 처리하는 서비스를 만듭니다. 새 파일 src/app/services/url.service.ts 추가:
import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { Url, Urls } from '../models/url.model'; import { environment } from '../../environments/environment'; @Injectable({ providedIn: 'root', }) export class UrlService { private apiUrl = environment.apiUrl; constructor(private http: HttpClient) {} shortenUrl(originalUrl: string): Observable<Url> { return this.http.post<Url>(`${this.apiUrl}/shorten`, { originalUrl }); } getAllUrls(): Observable<Urls> { return this.http.get<Urls>(`${this.apiUrl}/urls`); } getDetails(id: string): Observable<Url> { return this.http.get<Url>(`${this.apiUrl}/details/${id}`); } deleteUrl(id: string): Observable<Url> { return this.http.delete<Url>(`${this.apiUrl}/delete/${id}`); } }
URL 단축을 위한 새 구성요소 생성:
ng generate component shorten
구성요소의 HTML(src/app/shorten/shorten.comComponent.html)을 아래와 같이 업데이트합니다.
<div class="max-w-md mx-auto p-4 shadow-lg rounded-lg mt-4"> <h2 class="text-2xl font-bold mb-2">URL Shortener</h2> <form [formGroup]="urlForm" (ngSubmit)="shortenUrl()"> <div class="flex items-center mb-2"> <input class="flex-1 p-2 border border-gray-300 rounded mr-4" formControlName="originalUrl" placeholder="Enter your URL" required /> <button class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600" type="submit"> Shorten </button> </div> @if (urlForm.get('originalUrl')?.invalid && (urlForm.get('originalUrl')?.dirty || urlForm.get('originalUrl')?.touched)) { <div class="text-red-500" role="alert" aria-live="assertive"> @if (urlForm.get('originalUrl')?.errors?.['required']) { URL is required. } @if (urlForm.get('originalUrl')?.errors?.['pattern']) { Invalid URL format. Please enter a valid URL starting with http:// or https://. } </div> } </form> @if (errorMsg) { <div class="p-4 bg-red-100 rounded mt-4"> <p class="text-red-500">{{ errorMsg }}</p> </div> } @if (shortUrl) { <div class="p-4 bg-green-100 rounded"> <p>Shortened URL: <a class="text-blue-500 hover:text-blue-600" [href]="redirectUrl + shortUrl" target="_blank">{{ shortUrl }}</a> <button class="ml-2 px-2 py-1 bg-gray-200 text-gray-800 border border-slate-950 rounded hover:bg-gray-300" (click)="copyUrl(redirectUrl + shortUrl)">Copy</button> @if (copyMessage) { <span class="text-green ml-2">{{ copyMessage }}</span> } </p> </div> } </div> <div class="max-w-md mx-auto mt-4 p-2"> <h2 class="text-2xl font-bold mb-4">All URLs</h2> @if (isloading) { <div class="max-w-md mx-auto p-4 shadow-lg rounded-lg"> <div class="text-center p-4"> Loading... </div> </div> } @else if (error) { <div class="max-w-md mx-auto p-4 shadow-lg rounded-lg"> <div class="text-center p-4"> <p class="text-red-500">{{ error }}</p> </div> </div> } @else { @if (urls.length > 0 && !isloading && !error) { <ul> @for (url of urls; track $index) { <li class="p-2 border border-gray-300 rounded mb-2"> <div class="flex justify-between items-center"> <div> URL: <a class="text-blue-500 hover:text-blue-600" [href]="redirectUrl + url.shortUrl" target="_blank">{{ url.shortUrl }}</a> </div> <div class="flex justify-between items-center"> <button class="px-2 py-1 bg-blue-200 text-blue-800 rounded hover:bg-blue-300" (click)="showDetails(url.shortUrl)">Details</button> <button class="ml-2 px-2 py-1 bg-gray-200 text-gray-800 rounded hover:bg-gray-300" (click)="copyListUrl(redirectUrl + url.shortUrl, $index)">{{ copyIndex === $index ? 'Copied' : 'Copy' }}</button> <button class="ml-2 px-2 py-1 bg-red-200 text-red-800 rounded hover:bg-red-300" (click)="prepareDelete(url.shortUrl)">Delete</button> </div> </div> </li> } </ul> } @else { <div class="max-w-md mx-auto p-4 shadow-lg rounded-lg"> <div class="text-center p-4"> No URLs found. </div> </div> } } </div> @if (showDeleteModal) { <div class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50"> <div class="bg-white p-4 rounded shadow-lg"> <h3 class="text-xl font-bold mb-2">Confirm Deletion</h3> <p class="mb-4">Are you sure you want to delete this URL?</p> <button class="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600" (click)="confirmDelete()">Yes, Delete</button> <button class="px-4 py-2 bg-gray-300 text-gray-800 rounded hover:bg-gray-400 ml-2" (click)="showDeleteModal = false">Cancel</button> </div> </div> } @if (showDetailsModal) { <div class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50"> <div class="bg-white p-4 rounded shadow-lg"> <h3 class="text-xl font-bold mb-2">URL Details</h3> @if (isLoading) { <p class="mb-4">Loading...</p> } @else { <p class="mb-4">Short URL: <a class="text-blue-500 hover:text-blue-600" [href]="redirectUrl + selectedUrl.shortUrl" target="_blank">{{ selectedUrl.shortUrl }}</a></p> <p class="mb-4">Original URL: <a class="text-blue-500 hover:text-blue-600" [href]="selectedUrl.originalUrl" target="_blank">{{ selectedUrl.originalUrl }}</a></p> <p class="mb-4">Clicks: <span class="text-green-500">{{ selectedUrl.clicks }}</span></p> <p class="mb-4">Created At: {{ selectedUrl.createdAt | date: 'medium' }}</p> <p class="mb-4">Expires At: {{ selectedUrl.expirationDate | date: 'medium' }}</p> <button class="px-4 py-2 bg-gray-300 text-gray-800 rounded hover:bg-gray-400" (click)="showDetailsModal = false">Close</button> } </div> </div> }
양식 제출 및 API 상호 작용을 처리하려면 구성 요소의 TypeScript 파일(src/app/shorten/shorten.comComponent.ts)을 업데이트하세요.
import { Component, inject, OnInit } from '@angular/core'; import { UrlService } from '../services/url.service'; import { FormControl, FormGroup, ReactiveFormsModule, Validators, } from '@angular/forms'; import { Url } from '../models/url.model'; import { environment } from '../../environments/environment'; import { DatePipe } from '@angular/common'; import { Subject, takeUntil } from 'rxjs'; @Component({ selector: 'app-shorten', standalone: true, imports: [DatePipe, ReactiveFormsModule], templateUrl: './shorten.component.html', styleUrl: './shorten.component.scss', }) export class ShortenComponent implements OnInit { shortUrl: string = ''; redirectUrl = environment.apiUrl + '/'; copyMessage: string = ''; copyListMessage: string = ''; urls: Url[] = []; showDeleteModal = false; showDetailsModal = false; urlToDelete = ''; copyIndex: number = -1; selectedUrl: Url = {} as Url; isLoading = false; isloading = false; error: string = ''; errorMsg: string = ''; urlForm: FormGroup = new FormGroup({}); private unsubscribe$: Subject<void> = new Subject<void>(); urlService = inject(UrlService); ngOnInit() { this.urlForm = new FormGroup({ originalUrl: new FormControl('', [ Validators.required, Validators.pattern('^(http|https)://.*$'), ]), }); this.getAllUrls(); } shortenUrl() { if (this.urlForm.valid) { this.urlService.shortenUrl(this.urlForm.value.originalUrl).pipe(takeUntil(this.unsubscribe$)).subscribe({ next: (response) => { // console.log('Shortened URL: ', response); this.shortUrl = response.shortUrl; this.getAllUrls(); }, error: (error) => { console.error('Error shortening URL: ', error); this.errorMsg = error?.error?.message || 'An error occurred!'; }, }); } } getAllUrls() { this.isloading = true; this.urlService.getAllUrls().pipe(takeUntil(this.unsubscribe$)).subscribe({ next: (response) => { // console.log('All URLs: ', response); this.urls = response; this.isloading = false; }, error: (error) => { console.error('Error getting all URLs: ', error); this.isloading = false; this.error = error?.error?.message || 'An error occurred!'; }, }); } showDetails(id: string) { this.showDetailsModal = true; this.getDetails(id); } getDetails(id: string) { this.isLoading = true; this.urlService.getDetails(id).subscribe({ next: (response) => { // console.log('URL Details: ', response); this.selectedUrl = response; this.isLoading = false; }, error: (error) => { console.error('Error getting URL details: ', error); this.error = error?.error?.message || 'An error occurred!'; }, }); } copyUrl(url: string) { navigator.clipboard .writeText(url) .then(() => { // Optional: Display a message or perform an action after successful copy console.log('URL copied to clipboard!'); this.copyMessage = 'Copied!'; setTimeout(() => { this.copyMessage = ''; }, 2000); }) .catch((err) => { console.error('Failed to copy URL: ', err); this.copyMessage = 'Failed to copy URL'; }); } copyListUrl(url: string, index: number) { navigator.clipboard .writeText(url) .then(() => { // Optional: Display a message or perform an action after successful copy console.log('URL copied to clipboard!'); this.copyListMessage = 'Copied!'; this.copyIndex = index; setTimeout(() => { this.copyListMessage = ''; this.copyIndex = -1; }, 2000); }) .catch((err) => { console.error('Failed to copy URL: ', err); this.copyListMessage = 'Failed to copy URL'; }); } prepareDelete(url: string) { this.urlToDelete = url; this.showDeleteModal = true; } confirmDelete() { // Close the modal this.showDeleteModal = false; // Delete the URL this.deleteUrl(this.urlToDelete); } deleteUrl(id: string) { this.urlService.deleteUrl(id).subscribe({ next: (response) => { // console.log('Deleted URL: ', response); this.getAllUrls(); }, error: (error) => { console.error('Error deleting URL: ', error); this.error = error?.error?.message || 'An error occurred!'; }, }); } ngOnDestroy() { this.unsubscribe$.next(); this.unsubscribe$.complete(); } }
<router-outlet></router-outlet>
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; import { provideRouter } from '@angular/router'; import { routes } from './app.routes'; import { provideHttpClient } from '@angular/common/http'; export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideHttpClient(), ], };
import { Routes } from '@angular/router'; export const routes: Routes = [ { path: '', loadComponent: () => import('./shorten/shorten.component').then((m) => m.ShortenComponent), }, ];
Angular 및 Tailwind CSS를 사용하여 URL 단축기 애플리케이션을 성공적으로 구축했습니다. 이 프로젝트는 최신 프런트엔드 기술을 통합하여 기능적이고 세련된 웹 애플리케이션을 만드는 방법을 보여줍니다. Angular의 강력한 기능과 Tailwind CSS의 유틸리티 우선 접근 방식을 사용하면 반응성이 뛰어나고 효율적인 웹 애플리케이션을 쉽게 구축할 수 있습니다.
사용자 인증 등의 기능을 추가하여 이 애플리케이션을 자유롭게 확장하세요. 즐거운 코딩하세요!
GitHub 저장소를 방문하여 코드를 자세히 살펴보세요.
위 내용은 Angular 및 Tailwind CSS를 사용하여 URL 단축기 앱 구축의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!