Skip to content

Commit 0595678

Browse files
authored
Merge pull request #4 from ccaaffee/cafe
Cafe
2 parents 0348e1f + 90d3665 commit 0595678

File tree

9 files changed

+184
-20
lines changed

9 files changed

+184
-20
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/*
2+
Warnings:
3+
4+
- You are about to alter the column `status` on the `UserCafe` table. The data in that column could be lost. The data in that column will be cast from `Enum(EnumId(0))` to `Enum(EnumId(0))`.
5+
6+
*/
7+
-- AlterTable
8+
ALTER TABLE `UserCafe` MODIFY `status` ENUM('LIKE', 'DISLIKE') NULL;

prisma/schema.prisma

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,12 @@ model Cafe {
3939
enum PreferenceStatus {
4040
LIKE
4141
DISLIKE
42-
HOLD
4342
}
4443

4544
model UserCafe {
46-
status PreferenceStatus @default(HOLD)
47-
createdAt DateTime @default(now())
48-
updatedAt DateTime @updatedAt
45+
status PreferenceStatus?
46+
createdAt DateTime @default(now())
47+
updatedAt DateTime @updatedAt
4948
5049
user User @relation(fields: [userUuid], references: [uuid], onDelete: Cascade)
5150
userUuid String

src/cafe/cafe.controller.ts

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { User } from '@prisma/client';
2020
import { CreateCafeDto } from './dto/req/createCafe.dto';
2121
import { UpdateCafeDto } from './dto/req/updateCafe.dto';
2222
import { GetNearCafeListDto } from './dto/req/getNearCafeList.dto';
23-
import { GeneralCafeDto } from './dto/res/generalCafe.dto';
23+
import { GeneralCafeResDto } from './dto/res/generalCafe.dto';
2424
import { SetCafePreferenceDto } from './dto/req/setCafePreference.dto';
2525
import {
2626
ApiBearerAuth,
@@ -30,6 +30,8 @@ import {
3030
ApiUnauthorizedResponse,
3131
} from '@nestjs/swagger';
3232
import { PreferenceStatusDto } from './dto/res/preferenceStatus.dto';
33+
import { SwipeCafeListResDto } from './dto/res/switeCafeListRes.dto';
34+
import { GetSwipeCafeListDto } from './dto/req/getSwipeCafeList.dto';
3335

3436
@Controller('cafe')
3537
export class CafeController {
@@ -39,7 +41,7 @@ export class CafeController {
3941
summary: 'get near cafe list',
4042
})
4143
@ApiOkResponse({
42-
type: Array<GeneralCafeDto>,
44+
type: Array<GeneralCafeResDto>,
4345
description: 'Near cafe list based on given gps',
4446
})
4547
@ApiInternalServerErrorResponse({
@@ -48,15 +50,15 @@ export class CafeController {
4850
@Get('near')
4951
async getNearCafeList(
5052
@Query() query: GetNearCafeListDto,
51-
): Promise<GeneralCafeDto[]> {
53+
): Promise<GeneralCafeResDto[]> {
5254
return await this.cafeService.getNearCafeList(query);
5355
}
5456

5557
@ApiOperation({
5658
summary: 'get detailed cafe info',
5759
})
5860
@ApiOkResponse({
59-
type: GeneralCafeDto,
61+
type: GeneralCafeResDto,
6062
description: 'Detailed cafe information',
6163
})
6264
@ApiInternalServerErrorResponse({
@@ -72,7 +74,7 @@ export class CafeController {
7274
description: 'only available adminitrator or permitted person',
7375
})
7476
@ApiOkResponse({
75-
type: GeneralCafeDto,
77+
type: GeneralCafeResDto,
7678
description: 'Created cafe information',
7779
})
7880
@ApiUnauthorizedResponse({ description: 'Unauthorized' })
@@ -90,7 +92,7 @@ export class CafeController {
9092
summary: 'update cafe info',
9193
})
9294
@ApiOkResponse({
93-
type: GeneralCafeDto,
95+
type: GeneralCafeResDto,
9496
description: 'Updated cafe information with detail',
9597
})
9698
@ApiUnauthorizedResponse({ description: 'Unauthorized' })
@@ -174,4 +176,27 @@ export class CafeController {
174176
): Promise<PreferenceStatusDto> {
175177
return await this.cafeService.getCafePreference(user.uuid, cafeId);
176178
}
179+
180+
@ApiOperation({
181+
summary: 'get swiping near cafe list',
182+
description:
183+
'카페를 평가(스와이핑)하기 위해, 좌표를 기반으로 일정 거리 내에 있는 카페들을 반환합니다.',
184+
})
185+
@ApiOkResponse({
186+
type: SwipeCafeListResDto,
187+
description: 'Swiping target cafe list',
188+
})
189+
@ApiUnauthorizedResponse({ description: 'Unauthorized' })
190+
@ApiInternalServerErrorResponse({
191+
description: 'Internal Server Error',
192+
})
193+
@ApiBearerAuth('JWT')
194+
@Get('swipe/search')
195+
@UseGuards(JwtAuthGuard)
196+
async getSwipeCafeList(
197+
@GetUser() user: User,
198+
@Query() query: GetSwipeCafeListDto,
199+
): Promise<SwipeCafeListResDto> {
200+
return await this.cafeService.getSwipeCafeList(user, query);
201+
}
177202
}

src/cafe/cafe.repository.ts

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import { PrismaService } from 'src/prisma/prisma.service';
44
import { CreateCafeDto } from './dto/req/createCafe.dto';
55
import { UpdateCafeDto } from './dto/req/updateCafe.dto';
66
import { GetNearCafeListDto } from './dto/req/getNearCafeList.dto';
7-
import { GeneralCafeDto } from './dto/res/generalCafe.dto';
87
import { SetCafePreferenceDto } from './dto/req/setCafePreference.dto';
8+
import { GeneralCafeResDto } from './dto/res/generalCafe.dto';
9+
import { GetSwipeCafeListDto } from './dto/req/getSwipeCafeList.dto';
910

1011
@Injectable()
1112
export class CafeRepository {
@@ -43,10 +44,12 @@ export class CafeRepository {
4344
// TODO: SPATIAL INDEX 적용
4445
// TODO: 성능 안나온다 싶으면 MongoDB로 Migration도 고려
4546
// TODO: 물론, 관련 테스트는 무조건 할 것
46-
async getNearCafeList(query: GetNearCafeListDto): Promise<GeneralCafeDto[]> {
47+
async getNearCafeList(
48+
query: GetNearCafeListDto,
49+
): Promise<GeneralCafeResDto[]> {
4750
console.time('getNearCafeList');
4851

49-
const result = await this.prismaService.$queryRaw<GeneralCafeDto[]>`
52+
const result = await this.prismaService.$queryRaw<GeneralCafeResDto[]>`
5053
SELECT id, name, address, latitude, longitude, instagram, phone, createdAt
5154
FROM Cafe
5255
WHERE ST_Distance_Sphere(
@@ -101,4 +104,49 @@ export class CafeRepository {
101104
},
102105
});
103106
}
107+
108+
async getSwipeCafeList(
109+
userUuid: string,
110+
query: GetSwipeCafeListDto,
111+
page: number,
112+
take = 20,
113+
): Promise<{ data: GeneralCafeResDto[]; hasNextPage: boolean }> {
114+
const skip = (page - 1) * take;
115+
const limit = take + 1; // +1 to check for the next page
116+
const DISLIKE_EXPIRE_DAYS = 7;
117+
118+
const rawResult = await this.prismaService.$queryRaw<GeneralCafeResDto[]>`
119+
SELECT c.id, c.name, c.address, c.latitude, c.longitude, c.instagram, c.phone, c.createdAt
120+
FROM Cafe as c
121+
LEFT JOIN UserCafe as uc
122+
ON c.id = uc.cafeId
123+
AND uc.userUuid = ${userUuid}
124+
WHERE ST_Distance_Sphere(
125+
point(c.longitude, c.latitude),
126+
point(${query.longitude}, ${query.latitude})
127+
) <= ${query.radiusInMeter}
128+
AND (
129+
uc.status IS NULL
130+
OR
131+
(
132+
uc.status = 'DISLIKE'
133+
AND
134+
uc.updatedAt < DATE_SUB(NOW(), INTERVAL ${DISLIKE_EXPIRE_DAYS} DAY))
135+
)
136+
ORDER BY c.id
137+
LIMIT ${limit} OFFSET ${skip}
138+
`;
139+
140+
let hasNextPage = false;
141+
if (rawResult.length > take) {
142+
hasNextPage = true;
143+
rawResult.pop(); // 마지막 1개는 실제 응답으로 내려주지 않음
144+
}
145+
146+
return {
147+
data: rawResult,
148+
149+
hasNextPage,
150+
};
151+
}
104152
}

src/cafe/cafe.service.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ import { CafeRepository } from './cafe.repository';
77
import { CreateCafeDto } from './dto/req/createCafe.dto';
88
import { UpdateCafeDto } from './dto/req/updateCafe.dto';
99
import { GetNearCafeListDto } from './dto/req/getNearCafeList.dto';
10-
import { GeneralCafeDto } from './dto/res/generalCafe.dto';
10+
import { GeneralCafeResDto } from './dto/res/generalCafe.dto';
1111
import { SetCafePreferenceDto } from './dto/req/setCafePreference.dto';
12+
import { User } from '@prisma/client';
13+
import { SwipeCafeListResDto } from './dto/res/switeCafeListRes.dto';
14+
import { GetSwipeCafeListDto } from './dto/req/getSwipeCafeList.dto';
1215

1316
@Injectable()
1417
export class CafeService {
@@ -24,7 +27,9 @@ export class CafeService {
2427
return cafe;
2528
}
2629

27-
async getNearCafeList(query: GetNearCafeListDto): Promise<GeneralCafeDto[]> {
30+
async getNearCafeList(
31+
query: GetNearCafeListDto,
32+
): Promise<GeneralCafeResDto[]> {
2833
// 한국 내부 좌표인지 확인
2934
if (!this.isValidKoreanGPS(query.latitude, query.longitude)) {
3035
throw new BadRequestException(
@@ -97,4 +102,41 @@ export class CafeService {
97102

98103
return preference;
99104
}
105+
106+
async getSwipeCafeList(
107+
user: User,
108+
query: GetSwipeCafeListDto,
109+
page = 1,
110+
take = 20,
111+
): Promise<SwipeCafeListResDto> {
112+
if (!this.isValidKoreanGPS(query.latitude, query.longitude)) {
113+
throw new BadRequestException(
114+
'Wrong GPS coordinates (out of South Korea)',
115+
);
116+
}
117+
118+
const { data, hasNextPage } = await this.cafeRepository.getSwipeCafeList(
119+
user.uuid,
120+
query,
121+
page,
122+
take,
123+
);
124+
125+
if (page < 1) {
126+
throw new BadRequestException('Page must be greater than 0');
127+
}
128+
129+
if (take < 1 || take > 20) {
130+
throw new BadRequestException('Take must be between 1 and 20');
131+
}
132+
133+
const result: SwipeCafeListResDto = {
134+
data,
135+
nextPage: page + 1,
136+
cafeCount: data.length,
137+
hasNextPage,
138+
};
139+
140+
return result;
141+
}
100142
}

src/cafe/dto/req/getNearCafeList.dto.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,22 @@ import { IsNotEmpty, IsNumber, Max, Min } from 'class-validator';
55
export class GetNearCafeListDto {
66
@ApiProperty({
77
type: Number,
8-
description: "Cafe's Latitude",
8+
description: 'My Latitude',
99
example: 37.485772,
1010
})
1111
@IsNumber()
1212
@IsNotEmpty()
13-
// @Type(() => Number)
13+
@Type(() => Number)
1414
latitude: number;
1515

1616
@ApiProperty({
1717
type: Number,
18-
description: "Cafe's Longitude",
18+
description: 'My Longitude',
1919
example: 126.927983,
2020
})
2121
@IsNumber()
2222
@IsNotEmpty()
23-
// @Type(() => Number)
23+
@Type(() => Number)
2424
longitude: number;
2525

2626
@ApiProperty({
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { GetNearCafeListDto } from './getNearCafeList.dto';
3+
import { IsNumber, IsOptional } from 'class-validator';
4+
import { Type } from 'class-transformer';
5+
6+
export class GetSwipeCafeListDto extends GetNearCafeListDto {
7+
@ApiProperty({
8+
example: 1,
9+
description: 'Request page number(default: 1)',
10+
required: false,
11+
})
12+
@IsNumber()
13+
@IsOptional()
14+
@Type(() => Number)
15+
page: number;
16+
17+
@ApiProperty({
18+
example: 20,
19+
description: 'Number of pages to get(default: 20)',
20+
required: false,
21+
})
22+
@IsNumber()
23+
@IsOptional()
24+
@Type(() => Number)
25+
take?: number = 20;
26+
}

src/cafe/dto/res/generalCafe.dto.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
IsString,
88
} from 'class-validator';
99

10-
export class GeneralCafeDto {
10+
export class GeneralCafeResDto {
1111
@ApiProperty({
1212
type: Number,
1313
description: 'ID of Cafe',
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { GeneralCafeResDto } from './generalCafe.dto';
3+
4+
export class SwipeCafeListResDto {
5+
@ApiProperty({ type: [GeneralCafeResDto] })
6+
data: GeneralCafeResDto[];
7+
8+
@ApiProperty()
9+
nextPage: number;
10+
11+
@ApiProperty()
12+
cafeCount: number;
13+
14+
@ApiProperty()
15+
hasNextPage: boolean; // Indicates whether there is a next page
16+
}

0 commit comments

Comments
 (0)